本文同步发布于公众号:移动开发那些事如何设计一个伪无埋点的框架
在前面的文章:Android无埋点技术概览 中提到传统的无埋点有几大缺点:
- 埋点字段有限,没有办法携带精确的业务字段;
- 数据量太大,后台存储压力很大;
- View的唯一ID会随着页面的变化而变化,多个版本的数据需要在后台进行数据映射;
这几个缺点也是很多业务在进行技术方案选型时,不敢去选择无埋点的原因。那么有没有办法做到既可以利用无埋点的特性,并且又能满足业务的数据分析需求呢?
要能满足业务的数据分析需求,又能利用无埋点的特性,这意味着需要满足以下条件:
- 上报能携带业务字段;
- 上报可以按需选择;
- 上报时机由框架控制;
简化成一句话就是:业务控制上报内容与类型,框架决定上报时机,这也是本文要讲述的“伪”无埋点框架的核心;
如无特殊说明,文章里说的无埋点就是指代"伪"无埋点。
1 伪无埋点
在传统的代码埋点的方案,一般是业务同学在需要上报的时机,把要上报的具体的参数调用数据上报SDK的接口进行上报,如,当某个view
被点击时,上报点击事件:
// 业务在调用完这个上报接口后,由数据上报SDK把对应的数据上报到后台;
Report.report("event_click", {key:value});
而伪无埋点框架也是基于传统的代码埋点方案演变而来:由业务把某个View
要上报的业务参数和对应的业务标识设置给对应的View
,框架层负责在合适的时机去调用数据上报SDK的接口去进行上报,使用的伪代码为:
//步骤1 某个要上报的view,设置对应的上报参数
viewA.setReportParsms({key:value}});
// 步骤2,框架回调上报SDK接口进行上报
Report.report("event_name", {key:value});
那么这里的无埋点框架需要解决两个主要问题:
- 上报view的检测: 怎样检测到某个
View
需要做上报; - 上报参数的设置: 框架可以怎样获取到
View
要上报的业务参数;
2 上报view
的检测
要想检测到哪些View
需要做上报,需要有一个通用的机制来获取到所有的要检测到的View
,这里最常规的方案就是通过实现大量的自定义View
来实现,如自定义的ViewGroup
,自定义Text
等。只要需要做这些检测的View
都使用这些自定义的View
来实现,但这个常规的实现方案的成本很大:
- 框架的开发成本大,需要自定义大量的
View
,只要业务有用到的,都需要去自定义; - 业务的接入成本过高,需要将现在使用的组件替换为埋点框架的;
那这里有没有其他相对低成本的方案呢?那肯定是有,接下来我们来看一个比常规方案好一点的方案
2.1 进阶方案
大家都知道,Android
的页面是有生命周期方法的,不管是Activity
还是Fragment
在进入前台时,系统都会回调一些生命周期的方法,如onResume
,onPause
。那么我们是不是可以在系统回调这些方法时,对页面进行一个检测,获取到整个ViewTree
呢?只要获取到了ViewTree
,那么我们就可以通过某个标识来判断到哪些是需要做上报的View
后续只需要去判断这些View
的状态就可以了。这也是某埋点框架采用的方案。但这个方案会有个缺点:没有办法覆盖到所有的view变化的时机 像手动通过addView
,removeView
去更新ViewTree
的时候,这个方案就没有办法检测到。因此这个方案的时机还需要检测业务同学在某个时机,手动去调用检测的接口,来触发检测。这个进阶方案,也会存在几个缺点:
- 检测时机不可控,由框架和业务共同控制;
- 业务的接入成本有点高:
- 需要在页面的某些生命周期去调用框架的检测方法;
- 手动操作
viewtree
时,需要调用框架的检测方法; - 存在漏报的可能性(某个场景下,业务漏调用了检测方法);
这个进阶的方案虽然框架的开发成本和接入成本比常规方案有降低,但整体的成本还是比较大的,有没有更完美的方案呢?一个比较完美的方案应该是
- 能监测到所有
ViewTree
变化的时机; - 比较低成本的开发成本和接入成本;
接下来我们继续去探索一下完美的终极方案是怎样的?
2.2 终极方案
如果我们能想监测到所有ViewTree
的变化时机,这里只能是自定义View
。有同学看到这,肯定会想“就这?,前面已经说过自定义View
的成本很高,现在又说自定义View
,这不是有点自相矛盾吗?“。别急,我们一步步来拆解出可以怎样做到低成本的自定义View
的终极方案。
这里要做到低成本,关键在于自定义View
的个数尽可能要少,并且业务最好能无感知接入。所以这里的问题变成:怎样找到这些少量的View
。这里我们就只能从Android
的页面Activity
入手了。不管我们使用了什么开发框架,应用的页面一定是Activity
,而Activity
的根view
都是一个FrameLayout
(这里我们可以去查看Android
的源码去确认一下,其实从性能优化的减小布局层次的介绍中(如果布局layout.xml
中的根view
是FrameLayout
的话,推荐使用merge
标签)也能看出来了)。这里是不是可以自定义一个FrameLayout
就可以解决了。到这里其实答案就已经出来了。
我们通过自定义FrameLayout
来监听各种会引起view
变化的事件,如
- void dispatchWindowVisibilityChanged(int visibility)
- void onLayout(boolean changed, int left, int top, int right, int bottom)
有了这个根view
,我们就能获取到当前页面的所有view
,然后通过标识就能找到要上报的所有view
了。但现在问题又来了,怎样可以把业务的view
替换为这个自定义的view
呢?最低成本的方法其实就是运行时替换,在监听到Activity
的onResume
方法被调用时,动态把根view
替换掉就可以了。
综上,这里的终极方案为:
- 自定义
FrameLayout
,开发成本低(只需要一个自定义view
) - 运行时替换,业务接入成本低 (业务不用关心检测时机,只关注业务就可以)
通过这个方案,我们就能检测到所有的view
,并可以根据view
的标识来做一些上报的处理,但我们还需要解决怎样标识一个view
的问题?
3 上报参数的设置
要想把上报参数和对应的view
进行绑定,那么view
就需要有字段属性来存储我们设置的参数。一听到额外的字段属性,最常见的两种方案:
- 通过继承类,来增加属性;
- 通过扩展类,来增加属性;
继承类的方式也就是自定义View
的方式,这里的改造成本很大; 而扩展类的方式由于Android
开发使用的语言没有这一类的使用方式。因此最常见的这两种方案不适合这里,我们只能从源码出发,寻找有没有现在的字段可以使用;查看了一翻View
的源码后,发现有这么一个方法setTag
public class View {
/**
* Sets a tag associated with this view and a key. A tag can be used
* to mark a view in its hierarchy and does not have to be unique within
* the hierarchy. Tags can also be used to store data within a view
* without resorting to another data structure.
*
* The specified key should be an id declared in the resources of the
* application to ensure it is unique (see the <a
* href="{@docRoot}guide/topics/resources/more-resources.html#Id">ID resource type</a>).
* Keys identified as belonging to
* the Android framework or not associated with any package will cause
* an {@link IllegalArgumentException} to be thrown.
*
* @param key The key identifying the tag
* @param tag An Object to tag the view with
*
* @throws IllegalArgumentException If they specified key is not valid
*
* @see #setTag(Object)
* @see #getTag(int)
*/
public void setTag(int key, final Object tag) {
// If the package id is 0x00 or 0x01, it's either an undefined package
// or a framework id
if ((key >>> 24) < 2) {
throw new IllegalArgumentException("The key must be an application-specific "
+ "resource id.");
}
setKeyedTag(key, tag);
}
从这个方法的注释上可以看到这个方法是可以满足我们的需求,只是在使用的过程中需要注意这个key
一定要是Android
的资源id,再细看源码后,发现还有个需要注意的地方:tag
在View
内部是通过SparseArray
来存储的,而这个数据结构是非线程安全的,因此在调用setTag
方法一定要在主线程里去调用;
3.1 key值的定义
使用setTag
方法时的key
一定要是资源id,只需要在res
目录下的values
下新建一个ids.xml
的文件,里面的内容示例如下
<resources>
<item name="VIEW_PARAMS_KEY" type="id" />
</resources>
在使用时,就使用R.id.VIEW_PARAMS_KEY
来获取到具体的值就可以了;
4 总结
本文主要围绕Android
中设计一个伪无埋点框架需要解决的两个问题展开:
- 上报view的检测: 核心是通过自定义
FrameLayout
,并在运行时替换来解决; - 上报参数的设置: 核心是通过设置
View
的setTag
方法来进行设置和获取;
通过解决这两个关键问题,大家也就可以快速搭建起一个伪无埋点的框架了;在后面的文章里,会讲述如何基于这个框架,去搭建一个有效曝光的框架;
公众号: