本篇文章基于《网易乐得无埋点数据收集SDK》总结而成,关于网易乐得无埋点数据采集SDK的功能介绍以及技术总结后续会有文章进行阐述,本篇单讲SDK中用到的Android端AOP的实现。 随着流量红利时代过去,精细化运营时代的开始,网易乐得开始构建自己的大数据平台。其中,客户端数据采集是第一步。传统收集数据的方式是埋点,这种方式依赖开发,采集时效慢,数据采集代码与业务代码不解藕。 为了实现非侵入的,全量的数据采集,AOP成了关键,数据收集SDK探索和实现了一种Android上AOP的方式。
面向切向编程(Aspect Oriented Programming),相对于面向对象编程(ObjectOriented Programming)而言。 OOP的精髓是把功能或问题模块化,每个模块处理自己的家务事。但在现实世界中,并不是所有问题都能完美得划分到模块中,有些功能是横跨并嵌入众多模块里的,比如下图所示的例子。 图1-1 AOP概念说明示例 上图是一个APP模块结构示例,按照照OOP的思想划分为“视图交互”,“业务逻辑”,“网络”等三个模块,而现在假设想要对所有模块的每个方法耗时(性能监控模块)进行统计。这个性能监控模块的功能就是需要横跨并嵌入众多模块里的,这就是典型的AOP的应用场景。 AOP的目标是把这些横跨并嵌入众多模块里的功能(如监控每个方法的性能) 集中起来,放到一个统一的地方来控制和管理。如果说,OOP如果是把问题划分到单个模块的话,那么AOP就是把涉及到众多模块的某一类问题进行统一管理。 我们在开发无埋点数据收集是同样也遇到了很多需要横跨并嵌入众多模块里的场景,这些场景将在第二章(AOP应用情景)进行介绍。下面我们调研下Android AOP的实现方式。 AOP从实现原理上可以分为运行时AOP和编译时AOP,对于Android来讲运行时AOP的实现主要是hook某些关键方法,编译时AOP主要是在Apk打包过程中对class文件的字节码进行扫描更改。Android主流的aop 框架有:
除此之外,还有一些非框架的但是能帮助我们实现 AOP的工具类库:
Dexposed,Xposed的缺陷很明显,xposed需要root权限,Dexposed只对部分系统版本有效。 与之相比aspactJ没有这些缺点,但是aspactJ作为一个AOP的框架来讲对于我们来讲太重了,不仅方法数大增,而且还有一堆aspactJ的依赖要引入项目中(这些代码定义了aspactJ框架诸如切点等概念)。更重要的是我们的目标仅仅是按照一些简单的切点(用户点击等)收集数据,而不是将整个项目开发从OOP过渡到AOP。 AspactJ对于我们想要实现的数据收集需求太重了,但是这种编译期操作class文件字节码实现AOP的方式对我们来说是合适的。 因此我们实现Android上AOP的方式确定为:
在具体讲解实现技术之前,先看一下无埋点数据收集需求遇到的三个需要AOP的场景。 二、AOP应用情景 下面举出数据收集SDK通过修改字节码进行AOP的三个应用情景,其中情景一和二的字节码修改是方法级别的,情景三的字节码修改是指令级别的。 说明 收集页面数据时发现有些fragment是希望当作页面来看待,并且计算pv的(如首页用fragmen实现的tab)。而fragment的页面显示/隐藏事件需要根据:
这四个方法综合得出。 也就是说当项目中任一一个Fragment发生如上状态变化,我们都要拿到这个时机,并上报相关页面事件,也就是对Fragment的这几个方法进行AOP。 做法是:
示例 假设我们有一个Fragment1(空类,内部什么代码也没有)
经过扫描修改字节码后变为:
注:
说明 点击事件是分析用户行为的一个重要事件,Android中的点击事件回调大多是 也就是说当项目中任一一个控件被点击(触发了
达到的效果就是当APP中任何一个View被点击时,我们都可以在捕捉到这个时机,并且上报相关点击事件。 示例 假设有个实现接口的类
经过扫描修改字节码后变为:
注:
说明 弹窗显示/关闭事件,当然弹窗的实现可以是Dialog,PopupWindow,View甚至Activity,这里仅以Dialog为例。 当项目中任意一个地方弹出/关闭Dialog,我们都要拿到这个时机,即对Dialog.show/dismiss/hide这几个方法进行AOP。做法是:
假设项目中有一个代码(例如方法)块如下,其中某处调用了
经过扫描修改字节码后变为
注: 上面简单地列举了AOP在三种应用情景中达到的效果,下面介绍AOP的实现,实现的大致流程如下图所示: 图3-1 Android AOP实现流程 关键有以下几点: 1. 字节码插桩入口(图中1、3两个环节)。 我们知道Android程序从Java源代码到可执行的Apk包,中间有(但不止有)两个环节:
我们要想对字节码进行修改,只需要在javac之后,dex之前对class文件进行字节码扫描,并按照一定规则进行过滤及修改就可以了,这样修改过后的字节码就会在后续的dex打包环节被打到apk中,这就是我们的插桩入口(更具体的后面还会详述)。 2. bytecode manipulate(上图中第2个环节),这个环节主要做:
最后修改过字节码的class文件,将连同资源文件,一起打入Apk中,得到最终可以在Android平台可以运行的APP。 下面分别就插桩入口和ASM字节码操作两个方面进行详述。 如上所述,我们在Android 打包流程的javac之后,dex之前获得字节码插桩入口。 完整的Android 打包流程如下图所示: 图4-1 Android打包流程 说明: 图4-1中“dex”节点,表示将class文件打包到dex文件的过程,其输入包括1.项目java源文件经过javac后生成的class文件以及2.第三方依赖的class文件两种,这些class文件都是我们进行字节码扫描以及修改的目标。 具体来说,进行图4-1中dex任务是一个叫dx.jar的jar包,存在于Android SDK的sdk/build-tools/22.0.1/lib/dx.jar目录中,通过类似 :
的命令,进行将class文件打包为dex文件的步骤。 从上面的演示命令可以看出,dex任务是启动一个java进程,执行dx.jar中com.android.dx.command.Main类(当然对于multidex的项目入口可能不是这个类,这个再说)的main()方法进行dex任务,具体完成class到dex转化的是这个方法:
方法processClass的第二个参数是一个byte[],这就是class文件的二进制数据(class文件是一种紧凑的8位字节的二进制流文件, 各个数据项按顺序紧密的从前向后排列, 相邻的项[包括字节码指令]之间没有间隙),我们就是通过对这个二进制数据进行扫描,按照一定规则过滤以及字节码修改达到第二部分所描述的AOP情景。 那么我们怎么获得插桩入口呢? 对于Android Gradle Plugin 版本在1.5.0及以上的情况,Google官方提供了transformapi用作字节码插桩的入口。此处的Android Gradle Plugin 版本指的是build.gradle dependencies的如下配置:
此处1.5.0即为Android Build Gradle Plugin 版本。 关于transform api如何使用就不详细介绍了, 可自行查看API: http://tools.android.com/tech-docs/new-build-system/transform-api 参考热修复项目Nuwa的gradle插桩插件(使用transfrom api实现): http://blog.csdn.net/sbsujjbcy/article/details/50839263 那么对于Android Build Gradle Plugin 版本在1.5.0以下的情况呢? 下面我们介绍一种不依赖transform api而获得插桩入口的方法,暂且称为 hook dx.jar吧。
注:这种方式获得插桩入口也可参见博客《APM之原理篇》: http://blog.csdn.net/sgwhp/article/details/50239747 如何在一个标准的java进程(记得么?dex任务是启动一个java进程,执行dx.jar中com.android.dx.command.Main类的main()方法进行dex任务)中对特定方法进行字节码插桩? 这就需要运用Java1.5引入的Instrumentation机制。 java Instrumentation指的是可以用独立于应用程序之外的代理(agent)程序来监测和协助运行在JVM上的应用程序。这种监测和协助包括但不限于获取JVM运行时状态,替换和修改类定义等。 Instrumentation 的最大作用就是类定义的动态改变和操作。 Java Instrumentation两种使用方式: 方式一(java 1.5 ): 开发者可以在一个普通 Java 程序(带有 main 函数的 Java 类)运行时,通过
如此,则在目标main函数执行之前,执行agent jar包指定类的 premain方法 : |