关于编译时注解(APT)由浅入深有三部分,分别是:
1. 自定义注解处理器
例如 ButterKnife、Room 根据注解生成新的类。
2. 利用JcTree在编译时修改代码
像 Lombok 自动往类中新增 getter/setter 方法、往方法中插入代码行等。这种方式不推荐使用,因为只对 Java 代码有效,对 Kotlin 代码无效。
3. 自定义 Gradle 插件在编译时修改代码 (本文)例如一些代码插桩框架、日志框架、方法耗时统计框架等。
1. 环境搭建及Gradle配置
1.1 配置 Project 级的 build.gradle:
1 | buildscript { |
1.2 配置实现插件的 Module
创建一个 Java Library 或者 Android Library。
1.2.1 修改 module 的 build.gradle
修改 build.gradle 文件如下:
1 | apply plugin: 'java' |
1.2.2 创建 META-INF
创建 resources 文件夹,整体结构如下:
-
其中文件
your.gradle.plugin.name.properties
表明当前 module 下有一个gradle插件,插件的名称是your.gradle.plugin.name
。 -
一个 module 可以定义多个插件,每一个插件都需要在 gradle-plugins 文件夹下注册一个 xxx.properties 文件。
1.2.3 编辑 .properties 文件
注册文件的内容只有一行,用于关联该插件的具体实现类:
1 | implementation-class=your.plugin.implement.Class |
你需要把 your.plugin.implement.Class
替换为你自己的实现类全类名。
2. 实现插件
下文中的 Plugin
类指的是 org.gradle.api.Plugin 类,
Transform
类指的是 com.android.build.api.transform.Transform 类。
大致步骤:
-
实现一个
Plugin
的子类 和 一个Transform
的子类。 -
在子类的 apply 方法中,注册一个
Transform
实例。
我们在用到这个插件的 Module 的 build.gradle 中,添加 apply plugin: '插件名称'
实际上就是在调用 Plugin 实例的 apply 方法。
Transform 是什么?
Tansform是一个抽象类。每个 Transform 对象都是在打包过程中,从 .class 文件 生成 .dex 文件 期间,要执行的操作。我们可以用 Transform 处理注解信息,修改已存在的类和方法等。
2.1 实现 Plugin 子类
这一步简单,继承 Plugin 实现一个自定义的插件类,然后在 apply 方法中注册一个 Transformer 即可。
注意:该类的全类名需要和在xxx.properties文件中注册的全类名一样。
1 | class CustomPlugin : Plugin<Project> { |
CustomPlugin
可以替换为其他名称,只要和xxx.properties中注册的一致就行。
CustomTransform
也可以替换为你自己需要的名称。
2.2 实现 Transform 子类
2.2.1 Tansform 的重要方法
transform:
1 | /** |
getInputTypes:
1 | /** |
getScopes:
1 | /** |
getName:
1 | /** |
isIncremental
1 | /** |
2.2.2 实现 transform 方法
假设我们有这么一个需求:修改添加了 @DemoAnnotation 注解的方法,使得该方法在执行原始代码块之前和之后都打印一句话。例如将方法:
1 | void function() { |
修改为:
1 | void function() { |
transform 方法要做的步骤:
-
获取所有的输入 => inputs;
-
创建 ClassPool 对象 => classPool;
-
把系统类路径、inputs 包含的路径加入到 classPool 备用;
-
遍历 inputs:
- 获取每一个 input 的文件夹:
- 递归遍历文件夹,处理每一个类
- 获取当前文件夹的输出路径
- 将 input 文件夹复制到 输出文件夹
- 获取每一个 input 的 Jar:
- 同文件夹的处理方式
- 获取每一个 input 的文件夹:
具体实现:
1 | private var mProject: Project? = null |
2.2.3 实现 handleClass 方法
上面的 transform 方法是通用的流程,我们再来看怎么具体处理每一个文件。因为输入的都是 .class 文件,所以每个文件就有且只有一个类。
主要步骤:
-
根据文件读取到 Class 信息
-
获取所有定义的方法
-
遍历所有方法,并判断该方法是否需要修改
-
如果需要修改,修改该方法
-
将修改后的类写入文件
具体实现:
1 | private fun handleClass(directory: File, file: File) { |
2.2.4 实现 modifyMethod 方法
到这一步,我们可以根据自己的需求,对这个方法进行修改了。作为示例,我们为这个方法的执行前后都加上一句日志:
1 | private fun modifyMethod(method: CtMethod, clazz: CtClass) { |
insertAfter
方法会自动在所有 return 的地方都加上代码,开发者不用考虑提前return的问题。
结合上一篇 使用 APT 生成代码 的方法,可以在 modifyMethod 中插入自动生成的代码实现更强的功能。