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()));

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值