Android全埋点讲解

0
回复
120
查看
[复制链接]

491

主题

495

帖子

8314

安币

手工艺人

发表于 2019-5-23 10:30:01 | 显示全部楼层 |阅读模式
如果对本篇文章感兴趣,请前往,原文地址:http://www.apkbus.com/blog-873057-80118.html

### 什么是全埋点?也叫做无埋点,预先收集用户的所有行为数据,然后根据实际需求,从中提取行为数据。### 采集数据的点:- $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等。这几种方案的处理时机参考下图。![图片描述](/data/attachment/album/201905/23/102905ndvgjgadjehzt6vd.png)### 1、$AppViewScreen全埋点ActivityLifecycleCallbacks是Appliaction的一个内部接口,从 API 14 开始提供。在Appliaction中实现这个接口,便可以对所有Activity的生命周期进行监控。在onCreate中调用如下代码。``` registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {@Overridepublic void onActivityCreated(Activity activity, Bundle savedInstanceState) {}@Overridepublic void onActivityStarted(Activity activity) {}@Overridepublic void onActivityResumed(Activity activity) {   Log.e("Mr.S","resumed          " activity.getLocalClassName());}@Overridepublic void onActivityPaused(Activity activity) {    Log.e("Mr.S","paused          " activity.getLocalClassName());}@Overridepublic void onActivityStopped(Activity activity) {}@Overridepublic void onActivitySaveInstanceState(Activity activity, Bundle outState) {}@Overridepublic void onActivityDestroyed(Activity activity) {}        });```运行结果如下:```2018-12-20 12:52:37.377 12534-12534/? E/Mr.S: resumed          MenuActivity2018-12-20 12:52:40.385 12534-12534/com.ssy.qbd E/Mr.S: paused          MenuActivity2018-12-20 12:52:40.496 12534-12534/com.ssy.qbd E/Mr.S: resumed          HellowActivity2018-12-20 12:52:50.736 12534-12534/com.ssy.qbd E/Mr.S: paused          HellowActivity2018-12-20 12:52:50.744 12534-12534/com.ssy.qbd E/Mr.S: resumed          MenuActivity```### 2、 $AppStart/End全埋点因为系统没有直接的方法判断APP处于前台还是后台,所以我们需要一些假定逻辑来实现这个功能。![图片描述](/data/attachment/album/201905/23/102921n8tr0iyey3tty2sa.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,并开始代理。```   @Overridepublic 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.content2、DecorView但是onResume() 之后动态添加的View,就无法监听到了。所以我们又引入了3、ViewTreeObserver.OnGlobalLayoutListeener给rootViewe 添加ViewTreeObserver.OnGlobalLayoutListeener监听,收到回调(视图树发生变化的时候)我们会重新遍历一次rootview。当然在stop()的时候记得调用removeOnGlobalLayoutListener方法。免得不必要的内存问题。```@Overridepublic 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)不了解的可以先看一下这个(https://www.jianshu.com/p/5f869930ed55" target="_blank">Android 面向切面编程(AOP)]代码如下:```@Aspectpublic 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 等不过目前来看,这个方案很是很不错的。值得我们去实施。因为这是静态编译中学习成本相对最低的一个方案。  继续阅读全文



想在安卓巴士找到更多优质博文,可移步博客区

如果对本篇文章感兴趣,请前往,
原文地址:
http://www.apkbus.com/blog-873057-80118.html
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

领先的中文移动开发者社区
18620764416
7*24全天服务
意见反馈:1294855032@qq.com

扫一扫关注我们

Powered by Discuz! X3.2© 2001-2019 Comsenz Inc.( 粤ICP备15117877号 )