android.R.id.content

android.R.id.content 对应的 View 是一个 FrameLayout ,只有一个子元素,就是我们平时开发的时候,在 onCreate 方法中通过 setContentView 设置的 View。也即,当我们在 layout 文件中设置一个布局文件时,实际上该布局会被一个 FrameLayout 容器所包含,这个 FrameLayout 容器的 android:id 属性值就是 android.R.id.content。

需要引起我们注意的是,在不同的 SDK 版本下, android.R.id.content 所指的显示区域也是有所不同的。 具体的差异如下 :

• 在 SDK 14+ (Native ActionBar),该显示区域指的是 ActionBar 下面的那部分

• 在 Support Library Revision lower than 19,使用 AppCompat,则显示区域包含 ActionBar

• 在 Support Library Revision 19 (or greater),使 用 AppCompat,则显示区域不包含 ActionBar,即与第一 种情况相同 所 以 如 果 不 使 用 Support Library 或 使 用 Support Library 的最新版本,则 android.R.id.content 所指的区 域都是 ActionBar 以下的内容。

原理概述

在应用程序自定义的 Application 对象的 onCreate() 方法 中初始化埋点 SDK,并传入当前的 Application 对象。 SDK 拿 到 Application 对 象 之 后,就 可 以 通 过 registerActivityLifecycleCallback 方法注册 ActivityLife- cycleCallbacks。这 样 SDK 就 能 对 App 中 所 有 Activity 的生命周期事件进行集中处理(监控)了。在注册的 Application.ActivityLifecycleCallbacks 的 onActivityRe- sumed(Activity activity) 回调方法中,我们可以拿到当前 正 在 显 示 的 Activity,通 过 activity.findViewById (android.R.id.content) 方法就可以拿到整个内容区域对 应的 View ( 是一个 FrameLayout ) 了,本书有可能会用 RootView 和 ViewTree 来混称这个 View。然后 SDK 再逐 层遍历这个 RootView,并判断当前 View 是否设置了 onClickLisenter,如果已设置 onClickLisenter 并且又不 是我们自定义的 WrapperOnClickListener 类型,则通过 自 定 义 的 WrapperOnClickListener 代 理 当 前 View 设 置 的 View.OnClickListener,然 后 并 重 新 设 置 View 的 onClickLisenter 为 WrapperOnClickListener。Wrap- perOnClickListener 实现了 View.OnClickListener 接口, 在 WrapperOnClickListener 的 onClick 里会先调用 View 的原有 OnClickListener 处理逻辑,然后再调用埋点代码, 实现了“插入”埋点代码,从而达到自动埋点的效果。

实现步骤

完整的项目源码后续会 release 给大家。

引入 DecorView

通过测试发现,目前基于代理 View 的 OnClickListener 的方案无法采集 MenuItem 控件的点击事件。 这又是为什么呢? 其实,这是因为我们通过 android.R.id.content 获取到的 RootView 是不包含 Activity 标题栏的,也就是不包括 MenuItem 的父容器。所以当我们去遍历 RootView 时是 无 法 遍 历 到 MenuItem 的,因 此 无 法 代 理 其 OnClickListener,从而导致无法采集 MenuItem 的点击 事件。 下面我们使用 DecorView 来解决这个问题。 那什么是 DecorView 呢? DecorView 是整个 Window 界面的最顶层的 View(下图 中 编 号 为 0 的 View)。DecorView 只 有 一 个 子 元 素 为 LinearLayout(下 图 编 号 1),代 表 整 个 Window 界 面, 包含通知栏、标题栏、内容显示栏三块区域。这个 LinearLayout 里 有 两 个 FrameLayout 子 元 素。第 一 个 FrameLayout(下图编号 20)为标题栏显示界面。第二 个 FrameLayout(下图编号 21)为内容栏显示界面,就 是上面所说的 android.R.id.content。
所以,我们只需要将之前方案中 activity.findViewById (android.R.id.content) 换 成 activity.getWindow().get- DecorView(),就可以遍历到 MenuItem 了,也就可以自 动采集到 MenuItem 点击事件了。

引入 ViewTreeObserver.OnGlobalLayoutListener

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

ViewGroup rootView = findViewById(R.id.root- View);
AppCompatButton button = new AppCompatBut- ton(this);
button.setText(" 动态创建的 Button");
button.setOnClickListener(new View.OnClick- Listener() {
@Override public void onClick(View view) { }
}); rootView.addView(button);

此时,点击这个动态创建的 Button,是没有点击事件的。 这是因为我们是在 Activity 的 onResume 生命周期之前去 遍历整个 RootView 并代理其 View.OnClickListener 的。 如果是在 onResume 之后动态创建的 View,当时肯定是 无法遍历到的,后来我们又没有再次去遍历一次,所以它 的 mOnClickListener 就没有被代理过,所以点击时,我们是无法采集其点击事件的。 下 面 我 们 通 过 ViewTreeObserver.OnGlobalLayoutLis- tener 来解决这个问题。 那 什 么 是 ViewTreeObserver.OnGlobalLayoutListener 呢? OnGlobalLayoutListener 是 ViewTreeObserver 的 一 个 内部接口。当一个视图树的布局发生改变时,如果我们给 当 前 的 View 设 置 了 ViewTreeObserver.OnGlobalLay- outListener 监 听 器,就 可 以 被 ViewTreeObserver.On- GlobalLayoutListener 监 听 到(实 际 上 是 触 发 onGlobalLayout 回调)。所以,基于这个原理,我们可以 给 RootView 也 添 加 一 个 ViewTreeObserver.OnGlobal- LayoutListener 监听器,当收到 onGlobalLayout 回调时
(即视图树的布局发生变化,比如新的 View 被创建),我 们再重新去遍历一次 RootView,然后找到那些没有被代 理过 mOnClickListener 的 View 并进行代理。

关于 ViewTreeObserver.OnGlobalLayoutListener,建议 在页面退出的时候 remove 掉,即在 onStop 的时候调用 removeOnGlobalLayoutListener 方法。

缺点

• 由于使用反射,效率比较低,对 App 的整体性能有一定的影响,也可能会引入兼容性方面的风险 • Application.ActivityLifecycleCallbacks 要求 API 14+ • View.hasOnClickListeners() 要求 API 15+ • removeOnGlobalLayoutListener 要求 API 16+ • 无法直接支持采集游离于 Activity 之上的 View 的点击,比如 Dialog、PopupWindow ......

知识点

• android.R.id.content • Application.ActivityLifecycleCallbacks • DecorView • ViewTreeObserver.OnGlobalLayoutListener • 反射 • 代理

参考资料

[1] https://github.com/fengcunhan/AutoTrace [2] http://lingnanlu.github.io/2015/12/24/androidridcontent [3] https://stackoverflow.com/questions/24712227/android-r-id-content-as-container-for-fragment

注:该内容来自神策数据用户行为洞察研究院出品的《Android 全埋点解决方案》白皮书,查看完整白皮书可点击《Android 全埋点解决方案》

更多白皮书、报告、干货和案例,可以关注“神策数据”和“用户行为洞察研究院”公众号了解~