GeorgeYang'Blog

my technology blog

安卓无埋点方式统计方案

阅读:1382 创建时间:2017-11-21 19:23:19 tags:[android]

  • 前言

一个企业级的app总少不了app的埋点,一开始使用埋点的时候,直接在代码相应的位置写入埋点请求,但是总会因为各种埋点需求导致代码变更,埋漏埋错,而且上线了还不能修改,这是很不应该的,所以我们应该使用更科学更方便的埋点方案,那就是无埋点统计方案。

埋点的踩坑史,可参考美团:https://tech.meituan.com/mt-mobile-analytics-practice.html

  • 无埋点

如“无埋点”技术。所谓“无埋点”,是指不再使用笨拙的采集代码编程来定义行为采集的触发条件和后续行为,而是通过后端配置或前端可视化圈选等方式来完成关键事件的定义和捕获,可以大幅提升埋点工作的效率和易用性。在“无埋点”的场景下,数据监测工具一般倾向于在监测时捕获和发送尽可能多的事件和信息,而在数据处理后端进行触发条件匹配和统计计算等工作,以较好地支持关注点变更和历史数据回溯。

原话:https://www.zhihu.com/question/36411025/answer/144973846

  • android无埋点原理

根据android事件分发原理,和Activity中的UI布局是嵌套的,在activity中重写dispatchTouchEvent(MotionEvent ev)方法,监听ACTION_UP手指谈起事件,手指离开时,找到对应的view进行相应埋点操作。

  • 无埋点实践
  • 无埋点分三个步骤

  • 重写activity的dispatchTouchEvent方法

    public class BaseActivity extends AppCompatActivity {

     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_main);
    
         getWindow().setCallback(new Window.Callback() {
             @Override
             public boolean dispatchKeyEvent(KeyEvent event) {
                 return BaseActivity.super.dispatchKeyEvent(event);
             }
    
             @Override
             public boolean dispatchKeyShortcutEvent(KeyEvent event) {
                 return BaseActivity.super.dispatchKeyShortcutEvent(event);
             }
    
             @Override
             public boolean dispatchTouchEvent(MotionEvent event) {
                 int actionMasked = event.getActionMasked();
                 if (actionMasked != MotionEvent.ACTION_UP) {
                     return BaseActivity.super.dispatchTouchEvent(event);
                 }
                 findViewAndBury(getWindow().getDecorView());
                 return BaseActivity.super.dispatchTouchEvent(event);
             }
    
             .....
         }
    
         ...
     }
    
     private void findViewAndBury(View view) {
         ...
         //步骤二实现
     }
    

  • 根据点击坐标找到对应view

从activiy找到view有多种方法,本渣渣只找到两种方法:

一:根据xy坐标找对应的view,参考carl1990的github

 private void findViewAndBury(View rootView) {
         View view = searchClickView(rootView, ev);
         if (view != null && (view.hasOnClickListeners())|| hasOnTouchListener(view)) {
             //开始对view进行埋点
         }
 }

 private boolean isInClickView(View view, MotionEvent event) {
     float clickX = event.getRawX();
     float clickY = 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();
     if ((clickX >= x && clickX <= (x + width)) &&
         (clickY >= y && clickY <= (y + height))) {
         return true;  //这个条件成立,则判断点击时间发生在这个view区域内
     }
     return false;
 }

 private View searchClickView(View view, MotionEvent event) {
     View clickView = null;
     if (isInClickView(view, event) &&
         view.getVisibility() == View.VISIBLE) {  //这里一定要判断View是可见的
         if (view instanceof ViewGroup) {    //遇到一些Layout之类的ViewGroup,继续遍历它下面的子View
             ViewGroup group = (ViewGroup) view;
             for (int i = group.getChildCount() - 1; i >= 0; i--) {
                 View chilView = group.getChildAt(i);
                 clickView = searchClickView(chilView, event);
                 if (clickView != null) {
                     return clickView;
                 }
             }
         }
         clickView = view;
     }
     return clickView;
 }

 private boolean hasOnTouchListener(View view) {
     boolean result = false;
     Field listenerInfoField = null;
     try {
         listenerInfoField = Class.forName("android.view.View").getDeclaredField("mListenerInfo");
         if (listenerInfoField != null) {
             listenerInfoField.setAccessible(true);
         }
         Object myLiObject = null;
         myLiObject = listenerInfoField.get(view);

         // get the field mOnClickListener, that holds the listener and cast it to a listener
         Field listenerField = null;
         listenerField = Class.forName("android.view.View$ListenerInfo").getDeclaredField("mOnTouchListener");
         if (listenerField != null && myLiObject != null) {
             listenerField.setAccessible(true);
             View.OnTouchListener myListener = (View.OnTouchListener) listenerField.get(myLiObject);
             if (myListener != null) {
                 return true;
             }
         }
     } catch (NoSuchFieldException e) {
         e.printStackTrace();
     } catch (ClassNotFoundException e) {
         e.printStackTrace();
     } catch (IllegalAccessException e) {
         e.printStackTrace();
     }

     return result;
 }

二:根据view事件分发机制有关的TouchTarget链表获取点击view,参考得到Android团队无埋点方案

 ViewGroup.dispatchTouchEvent():
 public boolean dispatchTouchEvent(MotionEvent ev) {
     。。。
                 // 传递给touch目标  
             if (mFirstTouchTarget == null) {  
                 // 若没有Touch目标,则把自己当成一个View,调用  
                 handled = dispatchTransformedTouchEvent(ev, canceled, null,  
                         TouchTarget.ALL_POINTER_IDS);  
             } else {

     。。。
 }

细读源码可以知道,最终事件消费者,是没有mFirstTouchTarget的,参考Android的Touch系统简介(一)

 private void findViewAndBury(View rootView) {
         View buryView = null;
         try {
             Field field = ViewGroup.class.getDeclaredField("mFirstTouchTarget");
             field.setAccessible(true);
             Object mFirstTouchTarget = field.get(rootView);

             Field childField = mFirstTouchTarget.getClass().getDeclaredField("child") ;
             View childView = (View) childField.get(mFirstTouchTarget);

             while (childView!=null) {
                 if (childView instanceof ViewGroup) {
                     mFirstTouchTarget = field.get(childView);

                     childView = (View) childField.get(mFirstTouchTarget);
                 }
             }
             buryView = childView;
         } catch (Exception e) {
             e.printStackTrace();
         }

         //开始对buryView进行埋点
 }
  1. 实施埋点

根据以上流程,可以找到点击时对应的view,可以根据activity名viewid,viewTag等的形式进行组合,用字符串配对的方式定义事件名称,再发送埋点数据给服务器,这样就大功告成了!