Android自动化埋点技术探索-2

作者: 骑小猪看流星 | 来源:发表于2018-12-07 23:08 被阅读94次

上一篇文章 中,主要介绍了页面浏览事件、APP在前台还是后台这两类事件,在无埋点技术的理论分析和实践,本篇文章是自动化埋点技术探索的第二篇,主要介绍View层点击事件在无埋点技术上的理论分析和实践

View层的构建

在Android开发中,设置视图布局一般会在Activity的onCreate()方法中使用setContentView(),在这个方法里面传入具体的XML布局或者代码布局来生成View界面。XML布局在开发过程中是我们比较熟悉的,代码布局这种实现方式的代码量相较XML布局会变的异常巨大但是非常安全且运行效率会高于XML布局。言归正传,埋点技术最主要的功能,就是对View层上面的控件进行数据统计和分析,因此,了解View层的视图构建也就至关重要。

由于Android将视图展现的功能交给了Activity去完成,实际上View层的UI展现涉及到了Window、DecorView等等。它们之间的关系交互复杂,共同完成视图的显示以及与用户之间的交互;另外,UI视图的变化也是埋点技术讨论的一个焦点,Android针对视图树的变化,提出了ViewTreeObserver,来帮助开发者全局侦听视图树的更改。系统源码是宝贵的学习资源,下面就涉及到的知识点以及部分系统源码进行逐个分析

Window

首先看一下系统源码中Window的类结构,以及方法注释:

/**
 * Abstract base class for a top-level window look and behavior policy.  An
 * instance of this class should be used as the top-level view added to the
 * window manager. It provides standard UI policies such as a background, title
 * area, default key processing, etc.
 *
 * <p>The only existing implementation of this abstract class is
 * android.view.PhoneWindow, which you should instantiate when needing a
 * Window.
 */
public abstract class Window {
  ......
}

首先翻译一下系统源码关于Window的注释:Window是一个抽象基类,用于顶级窗口外观和行为策略。该类的实例应该用作添加到窗口管理器的顶级视图。它提供了标准的UI策略,如后台、标题区域、默认键处理等等

这个抽象类唯一的子类是PhoneWindow,在需要窗口时应该实例化该窗口。

中文的注释还是比较清晰易懂的,好了,这个PhoneWindow又是什么?打开 Android Studio 直接输入 PhoneWindow ,这个类实际上在IDE是找不到的,那这个类在那里?PhoneWindow 的源代码位置在: yousdk\sdk\sources\android-26\com\android\internal\policy\PhoneWindow

PhoneWindow类的内部持有一个 DecorView 的成员变量,而这个DecorView,系统源码也用一句英文注释描述了它的重要性:This is the top-level view of the window, containing the window decor. 翻译过来就是:这是窗口的顶层视图,包含窗口的装饰。因此,可以简单理解DecorView才是 view 的根布局。

DecorView

打开 Android Studio 直接输入 DecorView,IDE提示你会导入一个ViewPager的包,这明显不是我们想要的。那这个类的位置在那里?DecorView 的源代码位置在 yousdk\sdk\sources\android-26\com\android\internal\policy\DecorView,首先看一下系统源码DecorView的类结构:

/** @hide */
public class DecorView extends FrameLayout implements RootViewSurfaceTaker,
 WindowCallbacks {
  ......
}

可以看到DecorView是FrameLayout的子类,上面说到了DecorView可以被认为是Android视图树的根节点视图。DecorView作为顶级View,它内部包含一个LinearLayout,这个LinearLayout里面有三个部分,上面是个通知栏,中间是标题栏(根据Theme设置,有的布局没有),下面的是内容栏。合在一起代表的就是整个Window界面

注意:这个LinearLayout里有两个FrameLayout子元素

DcorView

20:为标题栏显示界面。只有一个TextView显示应用的名称。也可以自定义标题栏,载入后的自定义标题栏View将加入FrameLayout中。

21:为内容栏显示界面。就是setContentView()方法载入的布局界面,加入其中。

31:就是开发者自定义的XML,也就是说该布局会嵌入到里面的 FrameLayout

另外,这个 FrameLayout的Id是系统配置的,也就是android.R.id.content

ViewTreeObserver

首先看一下系统源码中ViewTreeObserver的类结构,以及方法注释:

/**
 * A view tree observer is used to register listeners that can be notified of global
 * changes in the view tree. Such global events include, but are not limited to,
 * layout of the whole tree, beginning of the drawing pass, touch mode change....
 *
 * A ViewTreeObserver should never be instantiated by applications as it is provided
 * by the views hierarchy. Refer to {@link android.view.View#getViewTreeObserver()}
 * for more information.
 */
public final class ViewTreeObserver {
    ......
}

首先翻译一下系统源码关于ViewTreeObserver 的注释:这个视图树的观察是用来注册可以通知全局监听器中视图树的更改。此类所有的事件包括但不限于:整棵树的布局,开始画,触摸模式更改等等。 ViewTreeObserver不应该由应用程序实例化,因为它是由视图层次结构提供的。如果想实例化可以参考getViewTreeObserver( )获得更多信息。

ViewTreeObserver全局监听又是如何实现的?

ViewTreeObserver内部提供了View的多种监听,每一种监听都有一个内部类接口与之对应,内部类接口全部保存在CopyOnWriteArrayList中,通过ViewTreeObserver.addXXXListener()来添加这些监听,源码如下:

public final class ViewTreeObserver {
    // Recursive listeners use CopyOnWriteArrayList
    private CopyOnWriteArrayList<OnWindowFocusChangeListener> mOnWindowFocusListeners;
    private CopyOnWriteArrayList<OnWindowAttachListener> mOnWindowAttachListeners;
    private CopyOnWriteArrayList<OnGlobalFocusChangeListener> mOnGlobalFocusListeners;
    private CopyOnWriteArrayList<OnTouchModeChangeListener> mOnTouchModeChangeListeners;
    private CopyOnWriteArrayList<OnEnterAnimationCompleteListener>
            mOnEnterAnimationCompleteListeners;

    // Non-recursive listeners use CopyOnWriteArray
    // Any listener invoked from ViewRootImpl.performTraversals() should not be recursive
    private CopyOnWriteArray<OnGlobalLayoutListener> mOnGlobalLayoutListeners;
    private CopyOnWriteArray<OnComputeInternalInsetsListener> mOnComputeInternalInsetsListeners;
    private CopyOnWriteArray<OnScrollChangedListener> mOnScrollChangedListeners;
    private CopyOnWriteArray<OnPreDrawListener> mOnPreDrawListeners;
    private CopyOnWriteArray<OnWindowShownListener> mOnWindowShownListeners;
    ......
}

以OnGlobalLayoutListener为例,首先是定义接口:

public interface OnGlobalLayoutListener {
        /**
         * Callback method to be invoked when the global layout state or the visibility of views
         * within the view tree changes
         */
        public void onGlobalLayout();
    }

这个注释很清楚: 当全局布局状态或视图的可见性发生改变时,调用回调接口。这个接口有一个回调方法 onGlobalLayout(),在开发中就是通过重写该方法,实现自己的逻辑。

接着将OnGlobalLayoutListener 添加到CopyOnWriteArray数组中:

/**
     * Register a callback to be invoked when the global layout state or the visibility of views
     * within the view tree changes
     *
     * @param listener The callback to add
     *
     * @throws IllegalStateException If {@link #isAlive()} returns false
     */
    public void addOnGlobalLayoutListener(OnGlobalLayoutListener listener) {
        checkIsAlive();

        if (mOnGlobalLayoutListeners == null) {
            mOnGlobalLayoutListeners = new CopyOnWriteArray<OnGlobalLayoutListener>();
        }
        //在这里进行添加
        mOnGlobalLayoutListeners.add(listener);
    }

这里通过 add 方法,添加一个对 view 布局发生改变的监听,传入的也就是 OnGlobalLayoutListener 接口对象,重写接口的 onGlobalLayout() 方法,系统会将传入的 OnGlobalLayoutListener 存在集合中。

既然有添加监听,与之对应的就有移除监听:

   /**
     * Remove a previously installed global layout callback
     *
     * @param victim The callback to remove
     *
     * @throws IllegalStateException If {@link #isAlive()} returns false
     * 
     * @see #addOnGlobalLayoutListener(OnGlobalLayoutListener)
     */
    public void removeOnGlobalLayoutListener(OnGlobalLayoutListener victim) {
        checkIsAlive();
        if (mOnGlobalLayoutListeners == null) {
            return;
        }
        mOnGlobalLayoutListeners.remove(victim);
    }

既然有添加和删除,那么应该还有事件的分发,

    /**
     * Notifies registered listeners that a global layout happened. This can be called
     * manually if you are forcing a layout on a View or a hierarchy of Views that are
     * not attached to a Window or in the GONE state.
     */
    public final void dispatchOnGlobalLayout() {
        // NOTE: because of the use of CopyOnWriteArrayList, we *must* use an iterator to
        // perform the dispatching. The iterator is a safe guard against listeners that
        // could mutate the list by calling the various add/remove methods. This prevents
        // the array from being modified while we iterate it.
        final CopyOnWriteArray<OnGlobalLayoutListener> listeners = mOnGlobalLayoutListeners;
        if (listeners != null && listeners.size() > 0) {
            CopyOnWriteArray.Access<OnGlobalLayoutListener> access = listeners.start();
            try {
                int count = access.size();
                for (int i = 0; i < count; i++) {
                    access.get(i).onGlobalLayout();
                }
            } finally {
                listeners.end();
            }
        }
    }

dispatchOnGlobalLayout 方法,会遍历存放 OnLayoutGlobalListener 对象的集合,然后调用 OnLayoutGlobalListener 对象的 onGlobalLayout() 方法,通知程序该事件发生。

本质上来说,ViewTreeObserver和View的关系 就是 熟悉的 观察者模式。当View发生变化的时候,及时通知ViewTreeObserver,因此,View的角色是被观察者、ViewTreeObserver的角色是 观察者;ViewTreeObserver通过addXxxListener 添加想要观察的事件,就拿上面的例子来说,想要观察 View 的全局布局状态变化,就是 view.getViewTreeObserver().addOnGlobalLayoutListener(…)

问题来了,View发生变化的时候,又是如何来通知 ViewTreeObserver?

这个问题的答案,需要分析 ViewRootImpl#performTraversals() 方法内部的代码,在 View 完成 layout(布局) 过程之后,通过调用 dispatchOnGlobalLayout() 方法,通知观察者“全局布局改变”事件发生了;这个方法调用以后,依次取出了集合中OnGlobalLayoutListener 接口对象,并调用接口的 onGlobalLayout() 方法,至此完成了事件的传递

总结

综上所述,针对View的点击事件在无埋点技术的实现就有以下步骤:

步骤一

在应用程序自定义的 Application 对象的 onCreate() 方法中初始化埋点 SDK,并传入当前的 Application 对象。SDK拿到Application对象之后,就可以通过registerActivityLifecycleCallback 方法注册 ActivityLife-cycleCallbacks。这样 SDK 就能对App 中所有Activity的生命周期事件进行集中监控。

注册之后,就可以拿到当前正在显示的 Activity,通过activity.findViewById(android.R.id.content) 方法就可以拿到整个内容区域对应的 View ( 是一个 FrameLayout )

然后 SDK 逐层遍历这个 RootView,并判断当前 View 是否设置了onClickLisenter,如果设置 onClickLisenter 且不是自定义的 WrapperOnClickListener 类型,则通过自定义的 WrapperOnClickListener 代理(如何代理,这个会在后面的文章做详细叙述)当前 View 设置的View.OnClickListener,然后重新设置 View onClickLisenter 为 WrapperOnClickListener。

Wrap-perOnClickListener 实现了 View.OnClickListener 接口,在WrapperOnClickListener 的 onClick 里会先调用 View的原有 OnClickListener 处理逻辑,然后再调用埋点代码,这样就实现了“插入”埋点代码,从而达到自动埋点的效果。

步骤二

但是通过步骤一 android.R.id.content 获取到的RootView 是不包含 Activity 标题栏的,也就是不包括MenuItem 的父容器。所以遍历 RootView 时是无法遍历到 MenuItem 的,因此无法代理其OnClickListener,从而导致无法采集 MenuItem 的点击事件。

所以,需要将之前方案中 activity.findViewById(android.R.id.content) 换成 activity.getWindow().get-DecorView(),这样就可以遍历到 MenuItem 、也就可以采集到 MenuItem 上面的点击事件。

步骤三

通过继续测试可以发现,步骤二有一个问题,即:目前该方案是无法采集 onResume() 生命周期之后动态创建的 View 的点击事件的。比如点击一个按钮,在其OnClickListener 里动态创建 一 个 Button,然后通过addView 添加到页面上此时,点击这个动态创建的 Button,是没有点击事件的。

这是因为 ,上面的步骤是在Activity 的 onResume 生命周期之前,去遍历整个 View 并代理其 View.OnClickListener 的。如果在 onResume 之后动态创建的 View,第一次是无法遍历到的,紧接着添加View之后,没有再次去遍历一次,所以它的 OnClickListener 就没有被代理过,因此这种情况下是无法采集其点击事件。

所以,基于这个问题,可以给 View 添加一个 ViewTreeObserver.OnGlobal-LayoutListener 监听,当收到 onGlobalLayout 回调时(即视图树的布局发生变化,比如新的 view 被创建),重新去遍历一次 View,然后找到那些没有被代理过 mOnClickListener 的 View 并进行代理。即可解决步骤二的问题,完成无埋点统计view的点击事件

文章部分内容选自:神策数据用户行为洞察研究院《安卓全埋点技术白皮书》,感谢分享!

如果这篇文章对您有开发or学习上的些许帮助,希望各位看官留下宝贵的star,谢谢。

Ps:著作权归作者所有,转载请注明作者, 商业转载请联系作者获得授权,非商业转载请注明出处(开头或结尾请添加转载出处,添加原文url地址),文章请勿滥用,也希望大家尊重笔者的劳动成果!

相关文章

  • Android自动化埋点技术探索-前言

    前言: 本篇文章是《Android自动化埋点技术探索》的第一篇,主要介绍埋点的基本概念以及几种埋点技术实现方式的原...

  • Android自动化埋点技术探索-2

    在 上一篇文章 中,主要介绍了页面浏览事件、APP在前台还是后台这两类事件,在无埋点技术的理论分析和实践,本篇文章...

  • Android埋点技术探索

    1.代码埋点: 埋点用于统计分析功能孩子操作的使用频率与使用习惯.统一定义一系列常量字符串来标识操作。因为可能埋的...

  • [iOS] iOS的埋点

    iOS自动化埋点探索

  • Android自动化埋点技术探索-1

    前言: 上一篇文章 主要介绍了埋点的基本概念以及几种埋点技术实现方式的原理和差异,本篇文章是自动化埋点技术探索的第...

  • aop的原理是什么

    iOS自动化埋点探索https://mp.weixin.qq.com/s/u-HmmrSAgtER1N2pKxCm...

  • iOS自动化埋点探索

    前言 最近跟同事花了点时间来思考可视化埋点,并没有什么突破性的进展,不过市面上很多关于可视化埋点的技术文章都在讲达...

  • iOS自动化埋点探索

    https://mp.weixin.qq.com/s/u-HmmrSAgtER1N2pKxCm0A 随着公司业务的...

  • MTFlexbox自动化埋点探索

    1. 背景 跨平台动态化技术是目前移动互联网领域的重点关注方向,它既能节约人力,又能实现业务快速上线的需求。经过十...

  • Android埋点技术总结

    1.埋点技术的分类 1.1 代码埋点:代码埋点是指在某个事件发生时调用数据发送接口上报数据。例如开发人员按照产品/...

网友评论

    本文标题:Android自动化埋点技术探索-2

    本文链接:https://www.haomeiwen.com/subject/piythqtx.html