如何对第三方依赖库中的activity界面添加打开关闭记录收集?
在项目实战中,有时会有记录页面打开和关闭的事件,并且上传到服务器用来做日后分析。普遍的做法是将处理写到BaseActivity中,但是当有第三方依赖库的界面时,则无法收集到。
编译插桩
编译插桩是在代码编译期间修改已有的代码或者生成新的代码。编译插桩可以在两处对代码进行改造:
在 .java 文件编译成 .class 文件时,APT、AndroidAnnotation 等就是在此处触发代码生成。
-
在 .class 文件进一步优化成 .dex 文件时,也就是直接操作字节码文件。这种方式功能更加强大,应用场景也更多。但是门槛比较高,需要对字节码有一定的理解。
而这节课主要讲的是通过第二种方式,使用的是ASM。而这让我想到了我使用过的AspectJ和APT都是第一种方式,正好没有使用过第二种,这节课程正好讲到了第二种。
一般情况下,使用编译插桩实现如下几种功能:
- 日志埋点
- 性能监控
- 动态权限控制
- 业务逻辑跳转校验是否已经登录
- 代码调试
ASM方式实现思路
1. 遍历项目中所有的.class文件
自定义Transform,获取所有.class文件引用。Transform的使用依赖Gradle Plugin,所以要先创建一个单独的 Gradle Plugin ,并在 Gradle Plugin 中使用Transform找出所有的.class文件。
2. 遍历到目标.class文件后,通过ASM洞体啊注入需要被插入的字节码
具体实现
创建主项目 ASMLifeCycleDemo,当前项目中只有一个 MainActivity,如下:
创建自定义 Gradle 插件
首先在 ASMLifeCycleDemo 项目中创建一个新的 module,并选择 Android Library 类型,命名为 asm_lifecycle_plugin。
将 asm_lifecycle_plugin module 中除了 build.gradle 和 main 文件夹之外的所有内容都删除。然后在 main 目录下分别创建 groovy 和 java 目录,结构如下:
因为 Gradle 插件是使用 groovy 语言编写的,所以需要新建一个 groovy 目录,用来存放插件相关的.groovy类。 但 ASM 是 java 层面的框架,所以在 java 目录里存放 ASM 相关的类。
然后,在 groovy 中创建目录 danny.jiang.plugin,并在此目录中创建类 LifeCyclePlugin.groovy 文件。在 LifeCyclePlugin 中重写 apply 方法,实现插件逻辑,因为是 demo 演示,所以我只是简单的打印 log 日志。
目录结构与代码如下:LifeCyclePlugin 实现了 gradle api 中的 Plugin 接口。 app module 的build.gradle 文件中使用此插件时,其 LifeCyclePlugin 的 apply 方法将会被自动调用。
接下来,将 asm_lifecycle_plugin module 的 build.gradle 中的内容全部删掉,改为如下内容:group 和 version 都需要在 app module 引用此插件时使用。
所有的插件都需要被部署到 maven 库中,可以选择部署到远程或者本地。课程中这里只是演示,所以只是将插件部署到本地目录中。具体地址通过 repository 属性配置,如图所示将其配置在项目根目录下的 asm_lifecycle_repo 目录下。
最后一步,创建 properties 文件。
在 plugin/src/main 目录下新建目录 resources/META-INF/gradle-plugins,然后在此目录下新建一个文件:danny.asm.lifecycle.properties,其中文件名 danny.asm.lifecycle 就是自定义插件的名称,稍后在 app module 中会使用到此名称。
在 .properties 文件中,需要指定自定义的插件类名 LifeCyclePlugin,如下所示:
至此,自定义 Gradle 插件就已经写完,现在可以在 Android Studio 的右边栏找到 Gradle 中点击 uploadArchives,执行 plugin 的部署任务:
构建成功之后,在 Project 的根目录下将会出现一个 repo 目录,里面存放的就是我们的插件目标文件。
测试自定义Gradle Plugin插件
图中 ① 处就是在自定义 Gradle 插件中 properties 的文件名 (danny.asm.lifecycle)。
图中 ② 处 dependencies 中的 classpath 是 group 值 + module 名 + version。
然后在命令行中使用 gradlew 执行构建命令,如果打印出我们自定义插件里的 log,则说明自定义 Gradle 插件可以使用:
备注:我这里按照课程步骤一直进行,但是编译失败,提示无法找到自定义的插件。我分别在Mac和Windows都进行了自定义Gradle Plugin,但是都提示未找到,我看到提示,寻找的路径并不是项目本地下的路径。Mac的路径是项目父路径下的asm_lifecycle_repo,而Windows则是.gradle/caches/.../.../asm_lifecycle_plugin-1.0.0.jar,不知为何。后续如果解决,会更新在此处。
接下来就需要实现遍历所有 .class 的逻辑,这部分功能主要依赖 Transform API。
Transform 可以被看作是 Gradle 在编译项目时的一个 task,在 .class 文件转换成 .dex 的流程中会执行这些 task,对所有的 .class 文件(可包括第三方库的 .class)进行转换,转换的逻辑定义在 Transform 的 transform 方法中。实际上平时我们在 build.gradle 中常用的功能都是通过 Transform 实现的,比如混淆(proguard)、分包(multi-dex)、jar 包合并(jarMerge)。
在 danny.jiang.plugin 目录中,新建 LifeCycleTransform.groovy,并继承 Transform 类。
LifeCycleTransform 需要实现抽象类 Transform 中的抽象方法,具体有如下几个方法需要实现:
Transform 主要作用是检索项目编译过程中的所有文件。通过这几个方法,我们可以对自定义 Transform 设置一些遍历规则,具体如下:
- getName:
设置我们自定义的 Transform 对应的 Task 名称。Gradle 在编译的时候,会将这个名称显示在控制台上。比如:Task :app:transformClassesWithXXXForDebug。
- getInputType:
在项目中会有各种各样格式的文件,通过 getInputType 可以设置 LifeCycleTransform 接收的文件类型,此方法返回的类型是 Set
ContentType 有以下 2 种取值:
CLASSES:代表只检索 .class 文件;
RESOURCES:代表检索 java 标准资源文件。
- getScopes()
这个方法规定自定义 Transform 检索的范围,具体有以下几种取值:
isIncremental() 表示当前 Transform 是否支持增量编译,我们不需要增量编译,所以直接返回 false 即可。
- transform()
在 自定义Transform 中最重要的方法就是 transform()。在这个方法中,可以获取到两个数据的流向。
inputs:inputs 中是传过来的输入流,其中有两种格式,一种是 jar 包格式,一种是 directory(目录格式)。
outputProvider:outputProvider 获取到输出目录,最后将修改的文件复制到输出目录,这一步必须做,否则编译会报错。
实现一个简易 LifeCycleTransform,功能是打印出所有 .class 文件。代码如下:
解释说明:
自定义的 Transform 名称为 LifeCycleTransform;
检索项目中 .class 类型的目录或者文件;
设置当前 Transform 检索范围为当前项目;
设置过滤文件为 .class 文件(去除文件夹类型),并打印文件名称。
将自定义的 LifeCycleTransform 注册到 Gradle 插件中
在 LifeCyclePlugin 中添加如下代码:
再次在命令行中执行 build 命令,可以看到 LifeCycleTransform 检索出的所有 .class 文件。
添加 ASM 依赖
在 asm_lifecycle_plugin 的 build.gradle 中,添加对 ASM 的依赖,如下:
创建自定义 ASM Visitor 类
在 asm_lifecycle_plugin module 中的 src/main/java 目录下创建包 danny.jiang.asm,并分别创建 LifecycleClassVisitor.java 和 LifecycleMethodVisitor.java。代码如下:
红框中,在 visitMethod 方法中,过滤出继承自 AppCompatActivity 的文件,并在 LifeCycleMethodVisitor.java 中对 onCreate 进行改造。
图中红框内是真正执行插入字节码的逻辑。可以看出 ASM 都是直接以字节码指令的方式进行操作的。
修改 LifeCycleTransform 的 transform 方法,使用 ASM
各种 Visitor 都定义好之后,就可以修改 LifeCycleTransform 的 transform 方法,并将需要插桩的字节码插入到 MainActivity.class 文件中:
重新部署自定义 Gradle 插件,并运行主项目
上面几步如果一切执行顺利,就可以在点击 uploadArchives 重新部署 LifeCyclePlugin。
备注:重新部署时,需要先在 app module 的 build.gradle 中将插件依赖注释,否则报错。
部署成功之后,重新在 app 中依赖自定义插件并运行主项目,当 MainActivity 被打开时,会在 logcat 中看到如下日志:
完成编译插桩改造
备注:混淆也是一个 Transform,叫作 ProguardTransform,它是在自定义的 Transform 之后执行。所以混淆对插桩不会有影响。