android应用中自动化埋点的实现,Android自动化埋点的实践

前几天在app里加上了按钮点击事件的自动埋点功能,这个功能的实现在面试中问过很多次,得到的答案都不尽如人意,归根到底是没有理解“自动”这个需求,自己也思考过一些方案,但是一直没有一个比较靠谱的实现方式,直到看了这篇文章,才豁然开朗。

思路是基于Android的事件传递机制,当手指触摸到屏幕时,当前的activity就接收到了一个按下的事件,这个事件通常会被activity传递给自己的子view,否则用户就点不了屏幕上的按钮了。因此可以给应用中所有的activity提供一个基类,在基类中对手指按下事件做统一的统计,这样就解决了在单个页面上埋点的问题。

既然要统计view的点击事件,那么首先要找到用户点击的是哪个view。但是各个页面的布局结构千差万别,怎么去定位这个target呢?如果你了解activity页面结构的话,这个问题就不是问题了。Activity有着跟html类似的布局结构,都有一个根布局,然后在根布局上再添加各种各样的view。在Activity中这个根就是DecorView,可以通过activity.getWindow().getDecorView()获得这个对象。简单来说,decorview里面包含两个子view,一个是titlelayout一个是contentlayout,一般来说titlelayout我们都直接隐藏的,因为会用到自己的titlebar,所以Activity里显示的内容就是contentlayout的内容,也就是setContentView()方法设置的layout,通过遍历我们可以得到这个页面上的所有view,然后通过view.getLocationOnScreen()可以得到这个view的大小和在屏幕上的位置,点击事件的位置可以通过event.getRawX()和event.getRawY()获得,这样我们就可以知道event是落在哪个view上了。

public boolean eventInView(View view, MotionEvent event){

if (view.getVisibility() == View.INVISIBLE || view.getVisibility() == View.GONE) {

return false;

}

int clickX = (int) event.getRawX();

int clickY = (int) event.getRawY();

int[] location = new int[2];

view.getLocationOnScreen(location);

int x = location[0];

int y = location[1];

int width = view.getWidth();

int height = view.getHeight();

if (clickX > x && clickX < (x + width) &&

clickY > y && clickY < (y + height)) {

return true;

}

return false;

}

然而,事情并没有那么简单。页面布局通常会进行嵌套,一个button可能是嵌套在一个父layout里,这样使用上面的方法判断下来的话,就有两个view获取到这个event了,实际情况可能是三个或者更多,这当然是个错误。怎么解决呢?思考一下我们是怎么来做布局的吧,如果现在有一个relativelayout,里面放了一个button,我们只给button注册点击事件,那relativelayout就不应该被统计到,此时只有button有onclick事件,relativelayout是没有的。如果反过来,点击relativelayout有事件,而点击button没有的话,就是relativelayout有onclick事件而button没有。因此,在得到event落在哪些view里以后,需要进行一个判断,哪个view有onclick事件,就认为哪个view实际被点击了。基于android的事件传递机制,我们应该从外到里从上到下进行遍历,只要外层view有点击事件,我们就可以结束遍历了。

怎么知道一个view是否有点击事件呢,android并没有提供类似于view.getOnClickListener()的api,这个问题只能通过反射来解决,具体实现依sdk版本不同而不同。实现如下:public View.OnClickListener getOnClickListener(View view){

if (view == null) {

return null;

}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {

return getOnClickListenerV14(view);

} else {

return getOnClickListenerV(view);

}

}

//Used for APIs lower than ICS (API 14)

private View.OnClickListener getOnClickListenerV(View view){

View.OnClickListener retrievedListener = null;

String viewStr = 'android.view.View';

Field field;

try {

field = Class.forName(viewStr).getDeclaredField('mOnClickListener');

retrievedListener = (View.OnClickListener) field.get(view);

} catch (NoSuchFieldException ex) {

Log.e('Reflection', 'No Such Field.');

} catch (IllegalAccessException ex) {

Log.e('Reflection', 'Illegal Access.');

} catch (ClassNotFoundException ex) {

Log.e('Reflection', 'Class Not Found.');

}

return retrievedListener;

}

//Used for new ListenerInfo class structure used beginning with API 14 (ICS)

private View.OnClickListener getOnClickListenerV14(View view){

View.OnClickListener retrievedListener = null;

String viewStr = 'android.view.View';

String lInfoStr = 'android.view.View$ListenerInfo';

try {

Field listenerField = Class.forName(viewStr).getDeclaredField('mListenerInfo');

Object listenerInfo = null;

if (listenerField != null) {

listenerField.setAccessible(true);

listenerInfo = listenerField.get(view);

}

Field clickListenerField = Class.forName(lInfoStr).getDeclaredField('mOnClickListener');

if (clickListenerField != null && listenerInfo != null) {

retrievedListener = (View.OnClickListener) clickListenerField.get(listenerInfo);

}

} catch (NoSuchFieldException ex) {

Log.e('Reflection', 'No Such Field.');

} catch (IllegalAccessException ex) {

Log.e('Reflection', 'Illegal Access.');

} catch (ClassNotFoundException ex) {

Log.e('Reflection', 'Class Not Found.');

}

return retrievedListener;

}好了,思路基本上就是这样,最后一个问题,我们应该记些啥内容?既然是自动埋点,肯定只能记一些通用的信息,有特殊需求的还是要手动去加。因此,我只记录了当前页面的类名和当前view的id:

Log.d(TAG,this.getClass().getSimpleName() + '-' + getResources().getResourceEntryName(view.getId()));

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值