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会根据当前屏幕快照生成一指纹,只有当前屏幕有变更时才会将当前页面快照数据发给用户分析平台。
2.6 其他技术点
2.6.1 多进程数据采集
子进程中只存在事件采集
和事件处理
两个模块,为了保证事件的连续性,数据的存储和上报则放到主进程来统一进行处理,这样也避免了数据库的同步问题,增加了数据的准确性,提升了系统性能。
因为事件采集是一种触发式的,所以我们在进程间通信上采用的是应用内广播,广播的优势是耦合度低,对子进程影响较小,同时性能相对来说可以接受。应用场景是技术选择的重要参考依据,所以这里并没有用Socket或者是AIDL来处理进程间通信。
2.6.2 多进程界面圈选
考虑到圈选是一个实时、持续的过程,所以SDK采用Socket方式实现进程间通信,所有子进程都将页面快照信息发给主进程,由主进程和服务端交互。
三、现存问题
当然,现阶段无埋点技术采用的字节码插桩方案还是存在一些短板,需要我们后续探索和解决。
- click监听如果是在layout中使用
android:onClick="xxxMethod"
设置的暂时无法进行采集。这个设置监听的方法是利用Java的反射原理,去寻找对应的Method,在WMDA中是通过拦截OnClickListener
点击事件来进行监听的,因此无法实现监听。 - 由于目前采用的是编译时插入埋点,所以不支持目前比较流行的RN框架。
- 同样因为是编译时插入埋点,所以对热更新的补丁支持可能也不到位。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
如何做好面试突击,规划学习方向?
面试题集可以帮助你查漏补缺,有方向有针对性的学习,为之后进大厂做准备。但是如果你仅仅是看一遍,而不去学习和深究。那么这份面试题对你的帮助会很有限。最终还是要靠资深技术水平说话。
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。建议先制定学习计划,根据学习计划把知识点关联起来,形成一个系统化的知识体系。
学习方向很容易规划,但是如果只通过碎片化的学习,对自己的提升是很慢的。
同时我还搜集整理2020年字节跳动,以及腾讯,阿里,华为,小米等公司的面试题,把面试的要求和技术点梳理成一份大而全的“ Android架构师”面试 Xmind(实际上比预期多花了不少精力),包含知识脉络 + 分支细节。
在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多。
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
roid的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。