Android全埋点

什么是全埋点?

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

采集数据的点:
  • $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等。这几种方案的处理时机参考下图。

1870221-1124ec8618eb60b2.png
静态处理方案

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处于前台还是后台,所以我们需要一些假定逻辑来实现这个功能。


1870221-ecb12a210ca2c06f.png

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

  • 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效果也是差不多的,问题依然存在。

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

2、静态代理

2.1、AspectJ
概念:

AOP: Aspect Oriented Programming 面向切面编程。

AOP是个概念,AspectJ 是它的一个具体实现。和Java配合使用。

AspectJ:核心是他的编译器(ajc),就做了一件事,讲 AspectJ 的代码在编译期插入到目标程序中。运行时没啥区别。ajc 会构建目标程序和AspectJ 代码的联系,在编译期将 AspectJ 代码插入被切出的 PointCut中。达到AOP的目的。

术语:

1、Advice:增强
也叫 通知。增强是织入目标类连接点的一段程序代码

2、JoinPoint:连接点
程序执行的某个特点的位置,比如 类的初始化前后,方法的调用前后等等。具有边界性质的点成为连接点。

3、PointCut:切入点
连接点 相当于 数据库的记录。 切入点 相当于 查询条件。
切入点和连接点不是一一对应的,一个切入点可以匹配多个连接点

4、Aspect:切面
切面有切点和连接点组成

5、Weaving:织入
将增强添加到目标类的具体连接点的过程。AOP 像一个织布机把目标类和增强缝在了一起
根据不同实现技术,三种织入的方式

  • 编译器织入,需要特殊的Java编译器
  • 类装载期织入,需要特殊的类加载器
  • 动态代理织入,在运行期为目标类添加增强生成子类的方式

5、Target:目标对象
增强逻辑的目标对象。

步骤:

1、首先定义一个表达式(PointCut)告诉程序我们要在哪里增加额外的操作。
通过这个表达式(PointCut),获得那些需要通知的方法(JoinPoint)。

2、我们要告诉程序这些方法(JointPoint)如何增强(Advice)

  • 什么时候?执行前?执行后?返回前?
  • 额外具体操作是干甚么?
    我们把这个两个步骤定义到一个地方(Aspect)。
    涉及到的被修改的对象就是目标对象(Tatget)。
    完成了上面的所有动作,总成织入(Weaving)。

未完待续···

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

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android无痕埋点是一种在Android应用程序中进行数据追踪的技术手段,不影响用户正常操作的同时,对用户行为进行统计和分析。 在传统的数据追踪方式中,开发人员通常需要手动在代码中添加埋点代码,这会增加代码的复杂性并且容易出错。而无痕埋点则通过修改应用程序的底层框架来实现自动追踪用户行为,无需手动插入埋点代码。 Android无痕埋点的原理是通过动态代理或Hook技术,拦截和修改应用程序的底层事件,如Activity生命周期、点击事件等,并将这些事件传递给埋点系统进行统计和分析。 该技术的优点在于,无痕埋点不会对用户体验造成影响,用户无感知地进行数据追踪。同时,由于无痕埋点是自动化的,开发人员不需要手动添加埋点代码,大大减少了开发和维护工作量。 然而,无痕埋点也存在一些限制和挑战。首先,为了实现无痕埋点,开发人员需要对Android底层框架有一定的了解。其次,由于对底层事件进行拦截和修改,无痕埋点可能会对应用程序的性能产生一定的影响,特别是在处理大量用户事件时。 总的来说,Android无痕埋点是一种实现数据追踪和分析的有效方法,通过自动化和无感知的方式,提供了更便捷和高效的数据采集方式。但同时也需要权衡好数据采集与用户体验之间的平衡,避免对应用程序性能和用户操作造成不必要的影响。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值