Android全埋点

作者: 猪_队友 | 来源:发表于2018-12-20 18:58 被阅读258次

    什么是全埋点?

    也叫做无埋点,预先收集用户的所有行为数据,然后根据实际需求,从中提取行为数据。

    采集数据的点:

    • $AppStart 冷启动➕热启动
    • $AppEnd 正常退出➕进入后台➕崩溃➕强杀等
    • $AppViewScreen 切换Activity
    • $AppClick (重点➕难点)控件的点击事件

    本质原理

    • 自动拦截 =>Android对View的点击处理
    • 自动插入 =>在编译阶段插入相应Java代码

    自动插入的流程如下

    JavaCode --> .java --> .class --> .dex
    

    具体方案

    • 动态代理

      • 代理View.OnClickListener
      • 代理Window.Callback
      • 代理View.AccessibilityDelegate
    • 静态代理

      • AspectJ 切面编程(AOP)
      • ASM
      • Javassist
      • APT 注解处理器

    Q:何为动态代理?
    A:在代码运行的时候去进行代理。比如我们常见的代理View.OnClickListener、Window.Callback、View.AccessibilityDelegate等

    Q:何为静态代理?
    A:通过Gradle Plugin在编译期间插入后者修改代码(.class文件)。比如AspectJ,ASM,Javassist,APT等。这几种方案的处理时机参考下图。

    静态处理方案

    1、$AppViewScreen全埋点

    ActivityLifecycleCallbacks是Appliaction的一个内部接口,从 API 14 开始提供。在Appliaction中实现这个接口,便可以对所有Activity的生命周期进行监控。

    在onCreate中调用如下代码。

     registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
                @Override
                public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
    
                }
    
                @Override
                public void onActivityStarted(Activity activity) {
    
                }
    
                @Override
                public void onActivityResumed(Activity activity) {
                   Log.e("Mr.S","resumed          "+activity.getLocalClassName());
    
                }
    
                @Override
                public void onActivityPaused(Activity activity) {
                    Log.e("Mr.S","paused          "+activity.getLocalClassName());
                }
    
                @Override
                public void onActivityStopped(Activity activity) {
    
                }
    
                @Override
                public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
    
                }
    
                @Override
                public void onActivityDestroyed(Activity activity) {
    
                }
            });
    

    运行结果如下:

    2018-12-20 12:52:37.377 12534-12534/? E/Mr.S: resumed          MenuActivity
    2018-12-20 12:52:40.385 12534-12534/com.ssy.qbd E/Mr.S: paused          MenuActivity
    2018-12-20 12:52:40.496 12534-12534/com.ssy.qbd E/Mr.S: resumed          HellowActivity
    2018-12-20 12:52:50.736 12534-12534/com.ssy.qbd E/Mr.S: paused          HellowActivity
    2018-12-20 12:52:50.744 12534-12534/com.ssy.qbd E/Mr.S: resumed          MenuActivity
    

    2、 $AppStart/End全埋点

    因为系统没有直接的方法判断APP处于前台还是后台,所以我们需要一些假定逻辑来实现这个功能。


    但是这些技术都无法解决以下两个问题

    • App多进程如何判断?
    • App奔溃被强杀怎么判断?

    解决方案也很简单,采用ContentProvider机制来解决多进程的问题。并通过数据库或者SharedPreferences来存储这些状态。

    对于奔溃强杀问题,我们引入Session这个概念。

    • 当一个页面退出了,如果 30 s 内没有新的页面打开那么我们认为应用进入后台了。
    • 当一个页面显示了,如果和上一个页面退出的时间超过了 30 s 我们认为 App 重新处于前台了。

    具体方案:

    1、注册ActivityLifecycleCallbacks,监听Activity的生命周期。并采用ContentProvider+SharedPreferences的方式进行进程间数据共享,注册ContentObserver来监听跨进程间的数据通信。

    2、页面退出的时候(onPause)启动一个倒计时 30 s ,如果 30 s 内没有新的界面显示触发 AppEnd 。如果有些页面那么,我们存储一个新的标记为来标记这个新页面(cp+sp)进行存储。然后通过ContentObserver 监听新页面标记位的改变,取消定时器。如果 30 s 内没有新的页面(按 home建 、退出、奔溃、强退等)我们会在下一次启动的时候补发这个AppEnd 事件。

    3、在下一次启动的时候,(onStart()),首先判断是否与上一个页面退出的时间间隔超过了 30 s ,如果没有超过 30 s 那么,那么无需补发 AppEnd,直接出发 AppScreen 事件。然后判断是否触发了 AppEnd,如果标志位是true,那么出发 AppStart。反之不触发。如果超过了 30 s 那么就去看看是否已经触发了 AppEnd,如果没有则先补发 AppEnd,然后在 AppStart,最后AppScreen。如果已经出发那么直接出发 AppStart,最后AppScreeen。

    3、AppClick全埋点

    这一小结是本文的重点,也是难点,也正是他复杂的情况和对性能的影响,产生了各种各样的方案。

    具体方案

    • 动态代理

      • 代理View.OnClickListener
      • 代理Window.Callback
      • 代理View.AccessibilityDelegate
    • 静态代理

      • AspectJ 切面编程(AOP)
      • ASM
      • Javassist
      • APT 注解处理器

    那么我们就详细的介绍一下这些方案的使用以及优劣点。

    3.1 代理View.OnClickListener

    代理的OnClickListenerer。

    public class MyWrapperOnClickListenerer implements View.OnClickListener {
    
        private View.OnClickListener onClickListener;
    
        public MyWrapperOnClickListenerer(View.OnClickListener onClickListener) {
            this.onClickListener = onClickListener;
        }
    
        @Override
        public void onClick(View v) {
    
            preClick();
            onClickListener.onClick(v);
            afterClick();
    
        }
    
        private void preClick() {
            Log.e("Mr.S", "preClick ");
        }
    
        private void afterClick() {
            Log.e("Mr.S", "afterClick ");
        }
    }
    

    获取rootView,并开始代理。

       @Override
                public void onActivityResumed(Activity activity) {
                    // Log.e("Mr.S", "resumed          " + activity.getLocalClassName());
    
                    ViewGroup rootView = activity.findViewById(android.R.id.content);
            
                 //ViewGroup rootView = (ViewGroup) activity.getWindow().getDecorView();
                    try {
                        setViewProxy(rootView);
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    } catch (InvocationTargetException e) {
                        e.printStackTrace();
                    }
                }
    

    循环遍历ViewGrop

     private void setViewProxy(ViewGroup viewGroup) throws IllegalAccessException, InvocationTargetException {
            int count = viewGroup.getChildCount();
            for (int i = 0; i < count; i++) {
                if (viewGroup.getChildAt(i) instanceof ViewGroup) {
                    setViewProxy((ViewGroup) viewGroup.getChildAt(i));
                } else {
                    hook(viewGroup.getChildAt(i));
                }
            }
        }
    

    通过反射 用MyWrapperOnClickListenerer 替换原来的OnClickListener。

        private void hook(View view) throws IllegalAccessException, InvocationTargetException {
    
            try {
                Method getListenerInfo = View.class.getDeclaredMethod("getListenerInfo");
                getListenerInfo.setAccessible(true);
                Object listenereInfo = getListenerInfo.invoke(view);
                try {
                    Class<?> listenerInfoClazz = Class.forName("android.view.View$ListenerInfo");
                    try {
                        Field mOnClickListener = listenerInfoClazz.getDeclaredField("mOnClickListener");
                        mOnClickListener.setAccessible(true);
                        View.OnClickListener originOnClickListener = (View.OnClickListener) mOnClickListener.get(listenereInfo);
                        if (originOnClickListener==null||originOnClickListener instanceof MyWrapperOnClickListenerer) {
                            return;
                        } else {
                            MyWrapperOnClickListenerer proxyOnClick = new MyWrapperOnClickListenerer(originOnClickListener);
                            mOnClickListener.set(listenereInfo, proxyOnClick);
                        }
    
                    } catch (NoSuchFieldException e) {
                        e.printStackTrace();
                    }
    
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }
    
    
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            }
    
        }
    
    

    我们的rootView可以:
    1、android.R.id.content
    2、DecorView

    但是onResume() 之后动态添加的View,就无法监听到了。所以我们又引入了

    3、ViewTreeObserver.OnGlobalLayoutListeener

    给rootViewe 添加ViewTreeObserver.OnGlobalLayoutListeener监听,收到回调(视图树发生变化的时候)我们会重新遍历一次rootview。当然在stop()的时候记得调用removeOnGlobalLayoutListener方法。免得不必要的内存问题。

                @Override
                public void onActivityResumed(Activity activity) {
    
                    // ViewGroup rootView = activity.findViewById(android.R.id.content);
                    rootView = (ViewGroup) activity.getWindow().getDecorView();
                    onGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
                        @Override
                        public void onGlobalLayout() {
                            try {
                                setViewProxy(rootView);
                            } catch (IllegalAccessException e) {
                                e.printStackTrace();
                            } catch (InvocationTargetException e) {
                                e.printStackTrace();
                            }
                        }
                    };
    
                    rootView.getViewTreeObserver().addOnGlobalLayoutListener(onGlobalLayoutListener);
                    try {
                        setViewProxy(rootView);
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    } catch (InvocationTargetException e) {
                        e.printStackTrace();
                    }
                }
    
    

    至此动态代理也就结束。我们的全埋点也基本实现。但是有没有发现一些问题呢?

    1、使用反射,效率比较低,对于性能会有影响,可能也会有兼容性问题
    2、Application.ActivityLifecycleCallbacks 需要 API 14+
    3、View.hasOnClickListeneers 需要 API 15+
    4、removeOnGlobalLayoutListener 需要 API 16+
    5、游离于Activity 之上的View的点击比如Dialog,PopupWindow无法被监视

    当然我们可以代理Window.Callback 和上面的原理相同。不过问题依然存在。
    代理View.AccessibilityDelegate效果也是差不多的,问题依然存在。

    面对这些问题,静态代理也是呼之欲出了。

    静态代理

    AspectJ 切面编程(AOP)
    不了解的可以先看一下这个Android 面向切面编程(AOP)

    代码如下:

    @Aspect
    public class TestAspect {
    
     @Pointcut("execution(* *(..))")
        public void pointcut() {
    
        }
    
      @Around("pointcut()")
        public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            String name = signature.getName();
            if (name.equals("onClick")) {
                Log.e("Mr.S", "preClick ");
    
                joinPoint.proceed();
                Log.e("Mr.S", "afterClick ");
            }else {
                return  joinPoint.proceed();
            }
    
    
            return null;
        }
    }
    

    结果:

    2018-12-21 15:43:59.245 30961-30961/com.ssy.qbd E/Mr.S: preClick 
    2018-12-21 15:43:59.259 30961-30961/com.ssy.qbd E/Mr.S: afterClick 
    

    一切感觉都很完美,但是也是缺点的:

    • 无法织入第三方的库
    • 由于定义的切点依赖程序语言,无法兼容Lambda语法
    • 会有一些兼容错误,比如 D8 、Gradle 4.x 等

    不过目前来看,这个方案很是很不错的。值得我们去实施。因为这是静态编译中学习成本相对最低的一个方案。

    未完待续···

    参考:神测数据-Android全埋点白皮书

    相关文章

      网友评论

        本文标题:Android全埋点

        本文链接:https://www.haomeiwen.com/subject/kqhykqtx.html