背景
本篇文章基于《网易乐得无埋点数据收集SDK》总结而成,关于网易乐得无埋点数据采集SDK的功能介绍以及技术总结后续会有文章进行阐述,本篇单讲SDK中用到的Android端AOP的实现。
随着流量红利时代过去,精细化运营时代的开始,网易乐得开始构建自己的大数据平台。其中,客户端数据采集是第一步。传统收集数据的方式是埋点,这种方式依赖开发,采集时效慢,数据采集代码与业务代码不解藕。
为了实现非侵入的,全量的数据采集,AOP成了关键,数据收集SDK探索和实现了一种Android上AOP的方式。
Android AOP
什么是AOP
面向切向编程(Aspect Oriented Programming),相对于面向对象编程(ObjectOriented Programming)而言。
OOP的精髓是把功能或问题模块化,每个模块处理自己的家务事。但在现实世界中,并不是所有问题都能完美得划分到模块中,有些功能是横跨并嵌入众多模块里的,比如下图所示的例子。
上图是一个APP模块结构示例,按照照OOP的思想划分为“视图交互”,“业务逻辑”,“网络”等三个模块,而现在假设想要对所有模块的每个方法耗时(性能监控模块)进行统计。这个性能监控模块的功能就是需要横跨并嵌入众多模块里的,这就是典型的AOP的应用场景。
AOP的目标是把这些横跨并嵌入众多模块里的功能(如监控每个方法的性能) 集中起来,放到一个统一的地方来控制和管理。如果说,OOP如果是把问题划分到单个模块的话,那么AOP就是把涉及到众多模块的某一类问题进行统一管理。
我们在开发无埋点数据收集是同样也遇到了很多需要横跨并嵌入众多模块里的场景,这些场景将在第二章(AOP应用情景)进行介绍。下面我们调研下Android AOP的实现方式。
Android AOP方式概述
AOP从实现原理上可以分为运行时AOP和编译时AOP,对于Android来讲运行时AOP的实现主要是hook某些关键方法,编译时AOP主要是在Apk打包过程中对class文件的字节码进行扫描更改。Android主流的aop 框架有:
- Dexposed,Xposed等(运行时)
- aspactJ(编译时)
除此之外,还有一些非框架的但是能帮助我们实现 AOP的工具类库:
- java的动态代理机制(对java接口有效)
- ASM,javassit等字节码操作类库
- (偏方)DexMaker:Dalvik 虚拟机上,在编译期或者运行时生成代码的 Java API。
- (偏方)ASMDEX(一个类似 ASM 的字节码操作库,运行在Android平台,操作Dex字节码)
Android AOP方式对比选择
Dexposed,Xposed的缺陷很明显,xposed需要root权限,Dexposed只对部分系统版本有效。
与之相比aspactJ没有这些缺点,但是aspactJ作为一个AOP的框架来讲对于我们来讲太重了,不仅方法数大增,而且还有一堆aspactJ的依赖要引入项目中(这些代码定义了aspactJ框架诸如切点等概念)。更重要的是我们的目标仅仅是按照一些简单的切点(用户点击等)收集数据,而不是将整个项目开发从OOP过渡到AOP。
AspactJ对于我们想要实现的数据收集需求太重了,但是这种编译期操作class文件字节码实现AOP的方式对我们来说是合适的。
因此我们实现Android上AOP的方式确定为:
- 采用编译时的字节码操作的做法
自己hook Android编译打包流程并借助ASM库对项目字节码文件进行统一扫描,过滤以及修改。
在具体讲解实现技术之前,先看一下无埋点数据收集需求遇到的三个需要AOP的场景。
AOP应用情景
下面举出数据收集SDK通过修改字节码进行AOP的三个应用情景,其中情景一和二的字节码修改是方法级别的,情景三的字节码修改是指令级别的。
Fragment生命周期
说明
收集页面数据时发现有些fragment是希望当作页面来看待,并且计算pv的(如首页用fragmen实现的tab)。而fragment的页面显示/隐藏事件需要根据:
onResume()
onPause()
onHiddenChanged(boolean hidden)
setUserVisibleHint(boolean isVisibleToUser)
这四个方法综合得出。
也就是说当项目中任一一个Fragment发生如上状态变化,我们都要拿到这个时机,并上报相关页面事件,也就是对Fragment的这几个方法进行AOP。
做法是:
- 对项目中所有代码进行扫描,筛选出所有Fragment的子类
- 对这些筛选出来的类的的onResumed,onPaused,onHiddenChanged,setFragmentUserVisibleHint这几个方法的字节码进行修改,添加上类似回调的逻辑
- 这样在项目中任何一个Fragment的这些回调触发的时候我们都可以得到通知,也即对Fragment的这几个切点进行了AOP。
示例
假设我们有一个Fragment1(空类,内部什么代码也没有)
public class Fragment1 extends Fragment {
}
经过扫描修改字节码后变为:
public class Fragment1 extends Fragment {
@TransformedDCSDK
public void onResume() {
super.onResume();
Monitor.onFragmentResumed(this);
}
@TransformedDCSDK
public void onPause() {
super.onPause();
Monitor.onFragmentPaused(this);
}
@TransformedDCSDK
public void onHiddenChanged(boolean var1) {
super.onHiddenChanged(var1);
Monitor.onFragmentHiddenChanged(this, var1);
}
@TransformedDCSDK
public void setUserVisibleHint(boolean var1) {
super.setUserVisibleHint(var1);
Monitor.setFragmentUserVisibleHint(this, var1);
}
}
注:
1. Monitor.onFragmentResumed等函数用于上报页面事件
2. @TransformedDCSDK 注解标记方法被数据收集SDK进行了字节码修改
用户点击事件
说明
点击事件是分析用户行为的一个重要事件,Android中的点击事件回调大多是View.OnClickListener的onClick方法(当然还有一部分是DialogInterface.OnClickListener或者重写OnTouchEvent自己封装的点击)。
也就是说当项目中任一一个控件被点击(触发了OnClickListener),我们都要拿到这个时机,并上报点击事件。也就是对View.OnClickListener的onClick方法进行AOP。做法是:
- 对项目中所有代码进行扫描,筛选出所有实现View.OnClickListener接口的类(匿名or不匿名)
- 对onClick方法的字节码进行修改,添加回调。
- 达到的效果就是当APP中任何一个View被点击时,我们都可以在捕捉到这个时机,并且上报相关点击事件。
示例
假设有个实现接口的类
public class MyOnClickListener implements OnClickListener {
public void onClick(View v) {
//此处代表点击发生时的业务逻辑
}
}
经过扫描修改字节码后变为: