WMDA移动端数据采集类型主要分三种:页面浏览事件、控件点击事件和自定义事件。作为无埋点解决方案,SDK核心点就是事件的无痕采集。 其中,这三种事件又对应不同的采集处理方式,WMDA通过不同的技术方案进行采集,最后将事件统一处理,然后存储、上报。
2.1.1 插桩入口
事件采集是无埋点技术的核心,其中WMDA对Fragment和控件点击事件拦截,使用的是自己开发的gradle插件wmda plugin,编译时使用ASM以字节码插桩的方式注入代码,实现事件的采集。
对于事件拦截,首先需要确定插入时机和待修改字节码文件。这里我们使用Transform API作为插桩入口,在Java Compiler之后,class文件打包成dex文件之前修改字节码文件。由于Transform API是在Gradle插件版本1.5.0出现的,所以项目开发中Gradle插件版本不能低于1.5.0。
classpath 'com.android.tools.build:gradle:1.5.0'
然后在transform中遍历并操作字节码文件,因为现在很多大型项目,都会进行组件化操作,拆分成多个Library。所以这里除了要修改我们的应用源码,还需要对第三方库中的字节码文件进行扫描操作。
void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException { inputs.each { TransformInput input -> input.directoryInputs.each { DirectoryInput directoryInput -> //对应用源码生成的class操作 } input.jarInputs.each { JarInput jarInput -> //对第三方库中class操作 } }}
同时我们也引入了注入白名单机制,如果app不希望某些包名下的类被注入代码,比如使用的第三方sdk,可以在项目级别根目录下创建wmda-rules.properties文件并填写不希望注入的类路径。以下为示例,可以添加多个,每行一条,以 “/” 结尾:
com/wuba/sdk/com/wuba/test/
2.1.2 页面事件
针对页面浏览事件,WMDA分两种不同的方式进行采集。
2.1.2.1 Activity采集 针对Activity
,WMDA采用的方式是使用LifecycleCallback
来监听页面的开启和关闭。 在页面开启时,拦截生命周期方法onResume
,然后在事件处理模块处理,将其格式化成事件结构,并进行存储上报。
@Overridepublic void onActivityResumed(Activity activity) { // 页面浏览事件采集处理 PageManager.getInstance().onActivityResumed(activity);}
不过该方案有个适配问题,在Android4.0(API14)以下,系统并不支持该方法。
注:目前App最低版本基本都是android 4.0及以上
在低版本中,我们也可以通过Hook方式拦截。通过拦截主线程的Instrumentation
实例,来实现低版本页面的监听。这块同时还需要考虑第三方插件也Hook该实例的情况,执行Hook前对应方法,保证对app中其他插件没有影响。缺点是如果其他SDK也使用了这种方式,可能会影响我们的拦截。
@Overridepublic void callActivityOnResume(Activity activity) { // 页面浏览事件采集处理 PageManager.getInstance().onActivityResumed(activity); //执行Hook前Instrumentation实例的onResume方法 oldInstrumentation.callActivityOnResume(activity);}
2.1.2.2 Fragment采集 针对Fragment
,由于Android系统并没有关于Fragment
生命周期的回调监听,所以这里WMDA通过Gradle插件,在编译时期,利用ASM库进行字节码操作,对Fragment
注入WmdaAgent
相应的页面采集方法,完成事件采集。在注入策略上,我们只需要对Fragment父类为下面两个的页面注入采集代码即可。
android/app/Fragmentandroid/support/v4/app/Fragment
对Fragment相关方法注入代码示例:
@OverrideMethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions); MethodVisitor wrappedMv = mv; if (wrappedMv != null) { // 在onResume中插入WmdaAgent.onFragmentResumed方法 if (name.equals("onResume") && desc.equals("()V")) { wrappedMv.visitCode() wrappedMv.visitVarInsn(Opcodes.ALOAD, 0) wrappedMv.visitMethodInsn(Opcodes.INVOKESTATIC, "com/wuba/wmda/autobury/WmdaAgent", "onFragmentResumed", "(Landroid/app/Fragment;)V") } }}
WmdaAgent
代码:
public static void onFragmentResumed(Fragment fragment) { // 页面浏览事件采集处理 PageManager.getInstance().onFragmentResumed(fragment);}
2.1.3 控件点击事件
关于点击事件的采集,WMDA在早期研发过程中采取的是Mixpanel开源方案。在上文中已经提到过,该方案开发效率不错,不过性能问题、Fragment页面采集不到问题、版本适配问题,导致该方案存在瓶颈和风险。
在后续的持续探索中,我们发现,使用Gradle插件在编译时埋点可以完美继承Mixpanel方案的各项优点,同时又可以规避其性能、数据准确性和版本适配问题。于是,在控件点击事件的采集上,我们调整了技术实现方案,从动态对View设置代理演进为编译时插入埋点代码。
WMDA对点击事件拦截支持常用的一些第三方框架,比如:
ButterKnife、databinding、AndroidAnnotations、RxBinding
具体的技术和之前的Fragment
插桩埋点是一样的,编译时对onClick
方法注入代码,以AOP方式对事件拦截处理。核心实现思路如下:
插件代码
@OverrideMethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions); MethodVisitor wrappedMv = mv; if (wrappedMv != null) { // 查找出方法名为onClick,入参为View的方法,注入WmdaAgent.onViewClick(View view) if (name.equals("onClick") && desc.equals("(Landroid/view/View;)V")) { wrappedMv.visitCode() wrappedMv.visitVarInsn(ALOAD, 1); wrappedMv.visitMethodInsn(INVOKESTATIC, "com/wuba/wmda/autobury/WmdaAgent", "onViewClick", "(Landroid/view/View;)V", false); } }}
WmdaAgent对应代码:
public static void onViewClick(View view){ // 控件点击事件采集处理 AutoEventManager.getInstance().onEvent(view);}
2.1.4 自定义事件
无埋点是WMDA的核心功能,但是由于业务场景特点,无埋点并不能完全满足所有业务场景需求,所以WMDA也提供了对手动埋点支持,使得WMDA在实际的使用中更加灵活,数据统计也更全面。
这部分没有特别关键的技术点,就是普通的代码埋点,这里就不做过多介绍了。
2.2 事件处理
事件收集完成之后,就会发送到事件处理线程,对原生的事件进行加工,以便服务端能更好的进行分析处理。在页面事件处理中,我们将页面的class全路径作为页面的特征值。APP_PAGE示例:
com.wuba.wmdademo.MainActivity
采集到页面事件后将其传入子线程处理,然后再提取出业务开发人员在页面onCreate
方法中设置的页面ID及页面自定义属性,将这些数据统一格式化,构造成页面浏览事件,传给事件存储模块。
在对控件事件处理中,我们面临一个最大的问题就是,**如何区分每一个控件,即如何定义控件的特征值。**在这里,我们借鉴了Mixpanel的方法,即将View自身的类名及index,以及其逐级向上的所有父View的类名和index统一收集起来,然后将所有遍历信息拼接起来,当做该View在当前页面的唯一特征值。控件的唯一标识:页面APP_PAGE + ViewPath + index
ViewPath举例:
/MainWindow/LinearLayout[0]/FrameLayout[1]/ActionBarOverlayLayout[0]#decor_content_parent/ContentFrameLayout[0]/LinearLayout[0]/ScrollView[0]/LinearLayout[0]/AppCompatButton
对于采集事件的后续处理,我们在UI性能上做了进一步优化。由于采用字节码插桩方式拦截事件,所以事件处理最耗时的点其实是在生成View特征值。在Android中,由于在子线程持有、操作view会引发内存泄漏问题。
在WMDA中,我们将构造特征值方法进行了拆分,在UI线程只进行对View数据提取,可以理解为定向的View遍历拷贝,除此之外不做任何其他耗时操作,然后将拷贝完成的ViewData传递给子线程,构造特征值,整合数据构造格式化的点击事件,最后再将事件传给存储模块。
点击事件处理时序图如下:
2.3 事件存储
事件处理完成之后,要交由存储模块进行本地持久化。在存储之前,先会检查存储策略,满足策略后再进行存储。
存储这里,使用的是本地SQLite存储Protobuf实例的二进制,然后使用AES-256进行加密存储。
2.4 事件上报
事件存储完成之后,会触发上报请求。在上报之前,WMDA会先检查上报策略,满足策略后进行上报。
上报这里为了缩小WMDA包,只使用了HttpUrlConnection
来处理网络操作。在数据上报的时候使用了GZIP+ProtoBuf来减少流量消耗,保证收集数据的同时,提升用户体验。
2.5 圈选模块
之前只是介绍数据采集方案,数据全量采集上报后,并不会直接分析处理,还需要一个圈选指标的过程。关于圈选的介绍,大家可以查看数据驱动增长:58无埋点用户行为分析实践之路 这篇的圈选部分,这里就不做重复介绍了。
通常,我们圈选时会在一个页面停留较长时间,这时其实是不需要一直将当前页面快照数据发送给服务端的,因为页面并没有变化。这块有一个优化策略,SDK会根据当前屏幕快照生成一指纹,只有当前屏幕有变更时才会将当前页面快照数据发给用户分析平台。
Android高级架构师
由于篇幅问题,我呢也将自己当前所在技术领域的各项知识点、工具、框架等汇总成一份技术路线图,还有一些架构进阶视频、全套学习PDF文件、面试文档、源码笔记。
- 330页PDF Android学习核心笔记(内含上面8大板块)
-
Android学习的系统对应视频
-
Android进阶的系统对应学习资料
- Android BAT部分大厂面试题(有解析)
好了,以上便是今天的分享,希望为各位朋友后续的学习提供方便。觉得内容不错,也欢迎多多分享给身边的朋友哈。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
需要这份系统化学习资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618156601)**
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!