最近在做埋点的SDK,这里记录一些思路方案和踩的坑,希望对其他小伙伴有所帮助~

Android端全埋点方案:

首先这里感谢神策的开源项目,在开发之前,重点阅读了神策出版的Android全埋点书籍,整理了很多知识点。

这里说的全埋点是指自动统计设置监听事件的的所有点击,页面的PV/UV以及应用程序使用的生命周期等。以下是本人根据书籍整理的一些大致知识点,具体的讲解和示例大家还是移步书籍。

1APPClick

一:代理View.OnClickListner 遍历View获取监听器然后代理插入埋点

问题: 不同组件绑定的点击监听器不同

绑定监听器的方式不同:setOn,XmlOnClick,ButterKnife,DataBinding(AspectJ无法支持lambda语法的点击事件)

OnResume生命周期之后动态创建的View

Dialog,popupWindow,BehaviorBottomSheet等游离于Activity之上的View需要额外处理

方案:    自定义Listener代理OnClickListener,获取DecorView遍历ViewTree找到设置了点击监听的View,代理监听器

ViewTreeObserver.OnGlobalLayoutListener解决Resume后动态创建的View。在OnActivityResumed中给当前的RootView设置监听,布局变化时候再去遍历一次。同时解决OnResume之后的DataBinding问题。注意需要在onStop中remove掉。

CheckBox等特殊组件需要判断是什么类型,然后使用对应的代理监听器更换,父类均是CompondButton的一系列。RadioGroup一样反射代理。RatingBar直接获取Listener。SeekBar的监听接口只需要关注onStopTrackingTOuch。反射和代理。Spinner直接获取监听类,element_content获取区分viewgroup等。ListView/GridView等类似Spinner。ExpandableListView区分GroupClick和ChildClick。较为复杂需处理position。

Dialog点击事件采集,获取dialog的decorWindow遍历执行前面反射获取和代理的一整套。在show之前,添加globleListener。

细节:element_content对于没有文本内容的组件可以设置contentDescription来代替。

缺点:     反射获取监听器,效率较低。影响性能。兼容风险

无法直接支持Dialog,PopupWindow和Behavior一系列等脱离Activity的视图,需要额外处理

二:代理Window.CallBack 通过act拿到window拿到Callback根据MotionEvent找到被点击的View对象。

原理:所有事件会回掉到window的callback中

方案:覆写dispatchTouchEvent,一样遍历,判断点击区域是否在当前View,结合可见/可点击条件确定。中间例如Spinner等控件需要结合方案一。

缺点:每次点击都需要遍历RootView。效率较低。影响性能。

三:代理View.AccessbilityDelegate

利用Android系统给障碍人群提供的基础服务

原理:view在点击之后,先走onClick,再走sendAccessibilityEvent,代理view的mAccessibilityDelegate对象。

问题:RatingBar/Seekbar/Spinner/Listview/不支持,需要结合方案一

方案:遍历view。反射回去delagate设置为代理的delagate,判断事件为点击事件。

缺点:反射,性能影响,兼容风险,辅助功能需要手动开启,再部分ROM上辅助功能失效。一样无法采集dialog等

四:透明层

自定义透明层View,添加到每个界面上层。通过OnTouchEvent结合坐标等确定点击的VIew。onActivityCreated中添加透明view,

缺点:每次点击都要遍历,效率低,dialog等无法采集

五:AspectJ(AOP)

缺点:无法织入第三方,无法兼容Lambda语法。兼容性问题,D8 Gradle4.x

六:ASM 字节码操作框架

操作.class文件,遍历目标方法,进行修改和原文件的替换,达到插入代码的木的。Gradle Transform 把输入的.class文件变为目标字节码文件。

问题:该方案目前无法采集通过xmlOnclick的点击事件,所以结合AspectJ的方案,通过注解的方式,然后在MethodVisitor中额外解析注解,有这个标记则插入对应方法字节码。

七:javassist 可以绕过编译,直接操作字节码,从而实现代码的注入。

同时也可以去生成一个新的类对象,通过完全手动的方式。处理java字节码的类库。

原理:创建Gradle Plugin,自定义Tranform 遍历.class文件,然后使用javassist的api去注入相应的方法代码。只是api变了。

比较:

1、源码API级别的面向开发者友好

2、使用反射机制,比ASM的classWorking原理更慢

八:AST APT是Annotation Processing Tool的缩写。注解处理器。是javac的一个工具,用来在编译时扫描和处理注解。 以java代码输入,以.java文件输出。

Abstract Syntax Tree 的缩写,即 "抽象语法树", 是编译器对代码第一步工作加工后的结果,是一个树形表示的源代码。源代码的每一个元素映射到一个节点或子节树

缺点:javac.tree相关api语法晦涩,理解难度大,要求一定的编译基础

apt无法扫描其他module,导致AST无法处理其他module

不支持lambda语法

带有返回值的方法,很难把埋点代码插入到方法之后

2AppViewScreen

问题:权限申请过程中会再次调用OnResume

          界面标题的获取存在差异

方案:注册ActivityLifeCycleCallBacks,在OnActivityResumed中上报。动态忽略和恢复上报处理特殊情况下界面。

         先拿Act本身的title,次是toolbar等,次是lable属性

3AppEnd APPStart

问题:多进程。异常崩溃。强杀情况下统计不到

方案:解决跨进程通信:contentProvider+contentObserver+SP

          30s 后台则触发End事件

缺点:崩溃或者强杀情况,如果用户不启动app,appEnd丢失,

最后,目前Android强推jetpack compose,这里的全埋点一套就不再适应了,因为compose的渲染机制,布局机制,触摸算法以及UI的具体写法,都是新的。后期神策包括其他埋点的sdk应该陆续也要去做这一部分的兼容,但是compose的学习成本已经挺高的了😂,希望有更优秀的对策。

RN侧全埋点方案

对于React Native混合项目而言,原生侧的全埋点实现之后,需要对react的页面也进行统计,这里是神策的RN侧的npm工具包,原理就是首先在安装脚本中完成对一些RN基层的文件的更改,更改文件列表如下:

android埋点书本 android 埋点框架_android

这部分更改就是为了在目标文件中插入上报埋点的代码,通过调用原生提供的module中的对应方法实现数据的上传。

 在实践这部分中需要注意的地方是,原生项目和RN项目的npm的依赖存在两种方式,一种是autolink:

//setting.gradle
//autolink 自动完成添加/依赖/addPacakage
apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)

一种是手动link:

//setting.gradle
include ':demo-react-native'
project(':demo-react-native').projectDir = new File(rootProject.projectDir, '../node_modules/demo-react-native/android')

//build.gradle中添加
implementation project(':demo-react-native')

//Application中
List<ReactPackage> packages = new ArrayList<>();
packages.add(new RNTrackAnalyticsPackage());

 为了确保原生端的module可以正常调用本地的依赖代码,需要手动check一下项目结构是否依赖了对应的模块。