android控件自动化埋点统计

  想到应用统计,平时我们使用的一般都是第三方的,比如百度统计,友盟统计,这几天公司要做自己的统计,于是乎也找了一些自动化埋点的博文学习使用。最后会给出参考链接,非常感谢。

现有的几种埋点技术的实现原理和优劣分析

 1.代码埋点:将收集数据的代码直接写在需要的地方,当用户点击某个控件或者打开某个页面时调用到该部分代码完成数据的收集。
  优势:准确性高,收集数据和发送数据都能精确控制,同时能方便的设置自定义属性,自定义控件,自定义View等。
  劣势:埋点工作量大,更新代价大。
 2.可视化埋点:根据配置文件收集用户行为,从而获取数据进行分析。
  优势:无须手动埋点,配置文件可动态更新。
  劣势:配置文件的配置比较耗时,弹出框,隐藏控件等行为不能收集。收集的数据比较简单,只能收集用户行为,不能收集到与行为相关的具体数据。
 3.无埋点:与可视化埋点基本一致。不同点在于可视化埋点是根据配置文件收集数据,无埋点是预先收集所有的用户行为,然后根据 配置文件来提取数据。无埋点可以通过修改配置文件追溯之前的用户行为数据。
 4.后端埋点:Sensors Analytics 这个平台有解决方案,优点是能收集到详细的与行为相关的数据,适用于电商等大平台。比如用户选择了一件商品,点击了加入购物车,那么可以收集到用户信息,商品信息,商品价格,商品库存,卖家等诸多信息。
 

埋点技术的选择

 1.代码埋点:既可以自己与后台定义接口,也可以使用第三方,常用的有友盟,百度统计等。
 2.可视化埋点和无埋点:移动端可以自己实现数据采集。第三方有诸葛IO,GrowingIO 。在知乎上查了关于这两个平台的信息,GrowingIO隐藏收费,官网并没有说到收费,但是使用15天后发邮件通知收费并停止数据采集和分析。诸葛IO免费模式的数据量是每月200万条,还有其他收费模式。
 3.后端埋点:Sensors Analytics

方案采用

 根据公司的需求以及对app的规模,只需实现控件按钮点击事件的监听,但是对控件的点击事件处都添加代码,工作量是有点大,所以我们采用控件自动化埋点的方法来统计,只需在BaseActvity中重写dispatchTouchEvent,在xml布局中给需要统计点击事件的控件添加tag标签即可,并不需要在每一个OnClickListener的地方加入拦截代码。

什么是自动化埋点

就是将界面的打开、关闭以及控件点击的log记录放到统一的地方去处理,而不用在许多业务逻辑中加入log代码。这块统一的监控代码需要做到如下的事情:
1.可以监控到界面打开或者关闭,并将这种操作记录到log中
2.当界面上的有控件被点击的时候,可以监控到哪个界面哪个控件被点击了,并将这些操作信息记录到log中
3.要能实现埋点的定制,即对需要埋点的控件或者界面才记录它们的操作log

原理解析

创建BaseActivity基类,实现控件的点击监听,根据UI布局的特性和Android点击事件传递机制实现。让创建的BaseActivity基类重写Activity的dispatchTouchEvent,当touch button时,获取到按下和抬起产生的MotionEvent对象,从而通过getRawX()和getRawY()方法获取到点击位置在界面中的坐标,然后搜索所有子view或者控件的布局区域示范包含点击位置,从而判断那个view或控件被点击了,再通过getTag()获取到我们控件的标签。

1.首选在我们的application中设置启动自动埋点

1
2
3
4
private void setAutoClickHook() {
BehaviorUtil.setAutoCollectEvent(true);
BehaviorUtil.setToastAutoCollectEvent(true);
}

2.然后在onAttachedToWindow()方法,该方法是在onResumed()方法之后,当view和window绑定的时候就会调用该方法,在该方法中我们通过getWindow().getDecorView()方法获取到根节点view的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
//获取到根节点的view
rootView = getWindow().getDecorView();
//控件在视图树上的根路径
rootViewTree = getPackageName() + "." + getClass().getSimpleName();
//前缀名 bigData
bigDataPrefix = getString(R.string.collection_tag);
//前缀名 bigData_
bigDataIngorePrefix = getString(R.string.collection_ignore_tag);
//前缀名 bigdata_ignore
bigDataEventPrefix = getString(R.string.collection_event_prefix);
}

3.其次我们便重写dispatchTouchEvent()方法,我们判断是否自动埋点,如果为true,我们就处理自动埋点的事件

1
2
3
4
5
6
7
8
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//判断是否自动埋点
if (BehaviorUtil.isAutoCollectEvent()) {
dealAutoCollect(ev);
}
return super.dispatchTouchEvent(ev);
}

4.在处理自动埋点的时候,我们就要判断touch状态,当为down的时候,我们就找到down点击位置的那个控件所在的视图

1
2
3
4
5
private ClickView findClickView(MotionEvent ev) {
Log.e(TAG, "bigdata-->findClickView");
ClickView clickView = new ClickView(rootView, rootViewTree);
return searchClickView(clickView, ev, 0);
}

5.ClickView是一个自定义的内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
private static class ClickView {
View view; //view
String viewTree; //view在视图树上的路径
String specifyTag;
int level = 0;//层级默认为0
int filterLevelCount = 3;//需要过滤的层级
ClickView(View view, String viewTree) {
this.view = view;
this.viewTree = viewTree;
}
}

6.通过点击的视图再去查找点击的view

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
private ClickView searchClickView(ClickView myView, MotionEvent event, int index) {
ClickView clickView = null;
View view = myView.view;
if (isInView(view, event)) { //遍历根view下的子view以及所有子view上的控件
// 当第二层不为LinearLayout时,说明系统进行了改造,多了一层,需要多剔除一层
myView.level++;
if (myView.level == 2 && !"LinearLayout".equals(view.getClass().getSimpleName())) {
myView.filterLevelCount++;
}
if (myView.level > myView.filterLevelCount) {
myView.viewTree = myView.viewTree + "." + view.getClass().getSimpleName() + "[" + index + "]";
}
Log.i(TAG, "bigdata-->tag = " + view.getTag());
// 如果Layout有设置特定的tag,则直接返回View,主要用于复合组件的点击事件
if (view.getTag() != null) {
// 主动标记不需要统计时,不进行自动统计
String tag = view.getTag().toString();
if (tag.startsWith(bigDataIngorePrefix)) {
return null;
} else if (tag.startsWith(bigDataPrefix)) {
if (tag.startsWith(bigDataEventPrefix)) {
myView.specifyTag = tag.replace(bigDataEventPrefix, "");
}
return myView;
}
}
if (view instanceof ViewGroup) { //遇到一些Layout之类的ViewGroup,继续遍历它下面的子View
if (view instanceof AbsListView) {
Log.i(TAG, "bigdata-->AbsListView ");
return null;
}
ViewGroup group = (ViewGroup) view;
int childCount = group.getChildCount();
if (childCount == 0) {
return myView;
}
for (int i = childCount - 1; i >= 0; i--) {
myView.view = group.getChildAt(i);
clickView = searchClickView(myView, event, i);
if (clickView != null) {
return clickView;
}
}
} else {
clickView = myView;
}
}
return clickView;
}

7.由于Activty的UI是层层嵌套的,isInView()方法就是通过根view层层遍历其下的子view以及子view上的控件,这些view和控件在屏幕中的坐标和高度我们是可以通过MotionEvent对象获取到的,从而搜索所有子view或者控件的布局区域示范包含“点击位置”,从而来判断那个view或控件被点击。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private boolean isInView(View view, MotionEvent event) {
//能被点击的view必然是可见的
if (view == null || view.getVisibility() != View.VISIBLE) {
return false;
}
int clickX = (int) event.getRawX();
int clickY = (int) event.getRawY();
//如下的view表示Activity中的子View或者控件
int[] location = new int[2];
view.getLocationOnScreen(location);
int x = location[0];
int y = location[1];
int width = view.getWidth();
int height = view.getHeight();
//返回true,则判断这个view被点击了
return clickX > x && clickX < (x + width) && clickY > y && clickY < (y + height);
}

8.由于是遍历view层其下的子view,所以isInView会被调用多次,myView.level也会++,代表当前遍历到view的层级,并且如果你在xml中给布局设置了tag,也会将相应的tag打印出来,如果没设置的话,getTag()返回null,查看打印日志我们可以看到层层遍历下取到的tag若没有定义则为null,并且funcName就是我们最后点击这个条目所在View的层级。
log

9.最后是BehaviorUtil的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class BehaviorUtil {
private static final String TAG = "BehaviorUtil";
/**
* 是否toast自动埋点的事件
*/
private static boolean isToast;
/**
* 是否自动埋点
*/
private static boolean isAutoCollectEvent;
/**
* 是否显示自动统计事件名称
*
* @return
*/
public static boolean isToastAutoCollectEvent() {
return isToast;
}
/**
* 设置是否显示自动统计事件
*
* @param toast
*/
public static void setToastAutoCollectEvent(boolean toast) {
isToast = toast;
}
/**
* 计次事件统计
*
* @param functionName 触发本次动作的功能点名,不能为空
*/
public static void clickEvent(String functionName) {
Log.d(TAG, "click event :" + functionName);
}
/**
* 是否自动埋点
*/
public static boolean isAutoCollectEvent() {
return isAutoCollectEvent;
}
/**
* 设置是否自动埋点
*/
public static void setAutoCollectEvent(boolean isAutoCollectEvent) {
BehaviorUtil.isAutoCollectEvent = isAutoCollectEvent;
}
}

自动化埋点存在的问题和难点
像有些布局我们可以通过该方法获取到被点击的控件,但是对于某些控件却找的不够准确,比如我在listview的header布局,那么它就找不到上面的控件,显示的fucName都是同一个,并且对于一些隐藏的控件,弹出窗口等都不好处理,那么这个UI路径作为唯一标识,那也就没办法自动埋点了,只能通过手动埋点来补充。
并且制动埋点的定制还需要自己写代码生成一个埋点列表,需要人工去查看比对,若一些控件布局经常发生变化,每一次都要重新找埋点控件的UI路径,也是相当麻烦的,并且在不同android版本会造成控件的UI路径不同,自动化埋点还存在很多的坑,还有很大的难点要克服。

参考文章

Android埋点技术分析调研
Android自动化埋点的实践
Android - 自动化埋点
Android无埋点数据收集SDK关键技术