得到Android无埋点方案细节分析

开源代码:DDAutoTracker

1. View唯一标识

id组成:ActivityName_LayoutFileName_idName

对应源码: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如何赋值?

一次简单的单点触控交互流程是这样的:

ACTION_DOWN(手指落下)
ACTION_MOVE(移动)
ACTION_MOVE
ACTION_MOVE
ACTION_MOVE
...
ACTION_UP(离开)

当用户触发ACTION_DOWN事件时,会执行如下逻辑,寻找消费当前事件的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 消费了触摸事件
          ..
          ..
          // 根据消费了触摸事件的View创建TouchTarget
           newTouchTarget = addTouchTarget(child, idBitsToAssign);
          ..
          ..
          break;
      }
}

假设ACTION_DOWN事件分发路径如下:
ViewGroup1
ViewGroup2
ViewGroup3
View

dispatch-path.png

路径中的每个ViewGroup都维护一个mFirstTouchTarget。

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

ACTION_DOWN事件分发过程中,路径中各ViewGroup成员变量mFirstTouchTarget赋值流程如下:

mFirstTouchTarget赋值流程

何时读取目标控件?

【得到Android】通过在Activity的window上调用window.setCallback() 接管窗口的事件派发,并在dispatchTouchEvent处理函数中添加读取目标控件的处理逻辑。如果接收到up事件,执行处理逻辑,通过ViewGroup TouchTarget链表,找到本次交互行为的目标控件。

读取目标控件的处理逻辑核心代码如下:

    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; //对于非Activity DecorView 的情况处理
        }
        View touchTarget;

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

            //无法找到touchTarget 相关信息
            if (touchTarget == null) return null;

            //已经找到touchTarget
            if (touchTarget == vg) break;

            boolean isVG = touchTarget instanceof ViewGroup;
            //已经找到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内mFirstTouchTarget赋值流程图,结合上面的算法与赋值流程图,可以反过来获取目标控件。


mFirstTouchTarget赋值流程

下述伪代码解释了目标控件获取过程:
ViewGroup2 = ViewHelper.findTouchTarget(ViewGroup1);
ViewGroup3 = ViewHelper.findTouchTarget(ViewGroup2);
View = ViewHelper.findTouchTarget(ViewGroup3);
View即为目标控件。

3. 布局文件名的获取

对应源码: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出发,向上回溯,读取ancestor的tag,读到则跳出循环。其中tag即为布局文件名。

Tag是如何塞进去的呢?

复习:

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

  • 当root不为null,attachToRoot为true时,表示将resource指定的布局添加到root中,添加的过程中resource所指定的的布局的根节点的各个属性都是有效的;

  • 如果root不为null,而attachToRoot为false的话,表示不将第一个参数所指定的View添加到root中;

  • 当root为null时,不论attachToRoot为true还是为false,显示效果都是一样的。当root为null表示我不需要将第一个参数所指定的布局添加到任何容器中,同时也表示没有任何容器来来协助第一个参数所指定布局的根节点生成布局参数。

方案:包装系统的mInflater,在调用inflate时,设置tag。

对应源码: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. 调用系统inflate,返回一个view;

  2. attachToRoot为false,resource指定的布局对应的view即为返回的view,将resource指定的布局文件名称设置为该view的tag;

  3. attachToRoot为true,resource指定的布局对应的view为返回的view的最后一个child;将resource指定的布局文件名称设置为该child的tag。

你可能感兴趣的:(得到Android无埋点方案细节分析)