得到Android無埋點(diǎn)方案細(xì)節(jié)分析

開源代碼:DDAutoTracker

1. View唯一標(biāo)識(shí)

id組成:ActivityName_LayoutFileName_idName

對(duì)應(yīng)源碼:ResourceHelper#getGlobalIdName

public static String getGlobalIdName(@NonNull View view) {
        int id = view.getId();
        ...
        try {
            Context context = view.getContext();
            // 獲取activityName
            String activityName = context.getClass().getSimpleName();
            // 獲取布局文件名
            String layoutFileName = getLayoutFileName(view);
            String idName;
            ...
            // 獲取id資源名
            idName = getResourceEntryName(context, id);
            ...

            return String.format("%s_%s_%s", activityName, layoutFileName, idName);
            ...
        }

    }

2. 定位交互控件:TouchTarget方案

TouchTarget如何賦值?

一次簡單的單點(diǎn)觸控交互流程是這樣的:

ACTION_DOWN(手指落下)
ACTION_MOVE(移動(dòng))
ACTION_MOVE
ACTION_MOVE
ACTION_MOVE
...
ACTION_UP(離開)

當(dāng)用戶觸發(fā)ACTION_DOWN事件時(shí),會(huì)執(zhí)行如下邏輯,尋找消費(fèi)當(dāng)前事件的TouchTarget。

if (actionMasked == MotionEvent.ACTION_DOWN){
    //如果是down事件,遍歷child,找到TouchTarget
    ..
    ..
    final View[] children = mChildren;
    for (int i = childrenCount - 1; i >= 0; i--) {
       final int childIndex = getAndVerifyPreorderedIndexchildrenCount, i, customOrder);
       final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
       ..
       ..
       if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
          // child 消費(fèi)了觸摸事件
          ..
          ..
          // 根據(jù)消費(fèi)了觸摸事件的View創(chuàng)建TouchTarget
           newTouchTarget = addTouchTarget(child, idBitsToAssign);
          ..
          ..
          break;
      }
}

假設(shè)ACTION_DOWN事件分發(fā)路徑如下:
ViewGroup1
ViewGroup2
ViewGroup3
View

dispatch-path.png

路徑中的每個(gè)ViewGroup都維護(hù)一個(gè)mFirstTouchTarget。

    // First touch target in the linked list of touch targets.
    private TouchTarget mFirstTouchTarget;

ACTION_DOWN事件分發(fā)過程中,路徑中各ViewGroup成員變量mFirstTouchTarget賦值流程如下:

mFirstTouchTarget賦值流程

何時(shí)讀取目標(biāo)控件?

【得到Android】通過在Activity的window上調(diào)用window.setCallback() 接管窗口的事件派發(fā),并在dispatchTouchEvent處理函數(shù)中添加讀取目標(biāo)控件的處理邏輯。如果接收到up事件,執(zhí)行處理邏輯,通過ViewGroup TouchTarget鏈表,找到本次交互行為的目標(biāo)控件。

讀取目標(biāo)控件的處理邏輯核心代碼如下:

    private View findActionTargets() {
        ViewGroup decorView = (ViewGroup) getWindow().getDecorView();
        int content_id = android.R.id.content;
        ViewGroup content = (ViewGroup) decorView.findViewById(content_id);
        if (content == null) {
            content = decorView; //對(duì)于非Activity DecorView 的情況處理
        }
        View touchTarget;

        ViewGroup vg = content;
        while (true) {
            //獲取指定vg的mFirstTouchTarget.child
            touchTarget = ViewHelper.findTouchTarget(vg);

            //無法找到touchTarget 相關(guān)信息
            if (touchTarget == null) return null;

            //已經(jīng)找到touchTarget
            if (touchTarget == vg) break;

            boolean isVG = touchTarget instanceof ViewGroup;
            //已經(jīng)找到touchTarget
            if (!isVG) break;

            //未找到touchTarget
            vg = (ViewGroup) touchTarget;
        }

        return touchTarget;
    }

其中,ViewHelper.findTouchTarget(ViewGroup)通過反射獲取指定ViewGroup的mFirstTouchTarget.child,源碼如下:

public static View findTouchTarget(@NonNull ViewGroup ancestor) {
        Preconditions.checkNotNull(ancestor);

        try {
            Field firstTouchTargetField = CoreUtils.getDeclaredField(ancestor, "mFirstTouchTarget");
            if (firstTouchTargetField == null) {
                logReflectException("mFirstTouchTarget");
                return ancestor;
            }

            firstTouchTargetField.setAccessible(true);
            Object firstTouchTarget = firstTouchTargetField.get(ancestor);
            if (firstTouchTarget == null) return ancestor;

            Field firstTouchViewField = firstTouchTarget.getClass().getDeclaredField("child");
            if (firstTouchViewField == null) {
                logReflectException("child");
                return ancestor;
            }

            firstTouchViewField.setAccessible(true);
            View firstTouchView = (View) firstTouchViewField.get(firstTouchTarget);
            if (firstTouchView == null) return ancestor;

            return firstTouchView;

        } catch (Exception e) {
            e.printStackTrace();

            return null;
        }
    }

再來看一下ViewGroup內(nèi)mFirstTouchTarget賦值流程圖,結(jié)合上面的算法與賦值流程圖,可以反過來獲取目標(biāo)控件。


mFirstTouchTarget賦值流程

下述偽代碼解釋了目標(biāo)控件獲取過程:
ViewGroup2 = ViewHelper.findTouchTarget(ViewGroup1);
ViewGroup3 = ViewHelper.findTouchTarget(ViewGroup2);
View = ViewHelper.findTouchTarget(ViewGroup3);
View即為目標(biāo)控件。

3. 布局文件名的獲取

對(duì)應(yīng)源碼:ResourceHelper#getLayoutFileName

    private static String getLayoutFileName(@NonNull View view) {

        String idNameSpace = (String) view.getTag(R.id.id_namespace_tag);
        if (!TextUtils.isEmpty(idNameSpace)) return idNameSpace;

        View tmp = view;
        while (tmp.getParent() != null && (tmp.getParent() instanceof View)) {
            View parent = (View) tmp.getParent();

            String space = (String) parent.getTag(R.id.id_namespace_tag);
            if (!TextUtils.isEmpty(space)) return space;

            tmp = parent;
        }

        return "";
    }

算法思路:從view出發(fā),向上回溯,讀取ancestor的tag,讀到則跳出循環(huán)。其中tag即為布局文件名。

Tag是如何塞進(jìn)去的呢?

復(fù)習(xí):

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot);

  • 當(dāng)root不為null,attachToRoot為true時(shí),表示將resource指定的布局添加到root中,添加的過程中resource所指定的的布局的根節(jié)點(diǎn)的各個(gè)屬性都是有效的;

  • 如果root不為null,而attachToRoot為false的話,表示不將第一個(gè)參數(shù)所指定的View添加到root中;

  • 當(dāng)root為null時(shí),不論attachToRoot為true還是為false,顯示效果都是一樣的。當(dāng)root為null表示我不需要將第一個(gè)參數(shù)所指定的布局添加到任何容器中,同時(shí)也表示沒有任何容器來來協(xié)助第一個(gè)參數(shù)所指定布局的根節(jié)點(diǎn)生成布局參數(shù)。

方案:包裝系統(tǒng)的mInflater,在調(diào)用inflate時(shí),設(shè)置tag。

對(duì)應(yīng)源碼:LayoutInflaterWrapper#inflate

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();

        final XmlResourceParser parser = res.getLayout(resource);
        try {
            View view = inflate(parser, root, attachToRoot);

            attachToRoot = (attachToRoot && root != null);
            if (!attachToRoot) {
                view.setTag(R.id.id_namespace_tag, ResourceHelper.getResourceEntryName(getContext(), resource));
                return view;
            }

            int childCount = root.getChildCount();
            View tagedView = root.getChildAt(childCount - 1);
            tagedView.setTag(R.id.id_namespace_tag, ResourceHelper.getResourceEntryName(getContext(), resource));
            return view;
        } finally {
            parser.close();
        }
    }

算法思路:

  1. 調(diào)用系統(tǒng)inflate,返回一個(gè)view;

  2. attachToRoot為false,resource指定的布局對(duì)應(yīng)的view即為返回的view,將resource指定的布局文件名稱設(shè)置為該view的tag;

  3. attachToRoot為true,resource指定的布局對(duì)應(yīng)的view為返回的view的最后一個(gè)child;將resource指定的布局文件名稱設(shè)置為該child的tag。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容