概述
最近在做一个在 Android 工程编译期间动态插入一些随机代码的需求,我选择的是 Gradle Transform 技术,想起好久没有写过博客了,就记录一下这方面的一些基本使用。
一般来说,在 Android 工程的编译期间可以通过一些技术来动态插入一些代码逻辑甚至生成一些新的 Class 类,具体技术有:
- APT(Annotation Processing Tool): 编译期注解处理技术,通过自定义注解和注解处理器来实现编译期生成代码的功能,并且将生成的代码和源代码一起编译成 class 文件。
- AspectJ: 是一种编译器,在编译期间,将开发者编写的 Aspect 程序织入到目标程序中,扩展目标程序的功能。
- Transform&Javassist: Transform 是 Android Gradle 提供的操作字节码的一种方式。它在 class 编译成 dex 之前通过一系列 Transform 处理来实现代码注入。Javassist 可以方便地修改 .class 文件,关于 Javassist 的用法可以参考 Javassist用法。
这里还可以看看 AOP 和 IOC 的一些概念,参考 AOP-IOC概述。
Android Gradle 工具从 1.5.0-beta1 版本开始提供了 Transform API 工具,它可以在将 .class 文件转换为 dex 文件之前对其进行操作。可以通过自定义 Gradle 插件来注册自定义的 Transform,注册后 Transform 会包装成一个 Gradle Task 任务,这个 Task 在 compile task 执行完毕后运行。
依赖如下:
1 | implementation 'com.android.tools.build:gradle:4.1.1' |
当在buildSrc中开发插件时,其build.gradle脚本内容如下:
1 | apply plugin: 'groovy' |
Transform处理流程如下图(图片来于网络):
Transform
先看看Transform类,这是一个abstract类,实现自定义 Transform task 需要重写它,一般需要重写的方法有:
1 | class InjectTransform extends Transform { |
getName
指明 Transform 的名字,也对应了该 Transform 所代表的 Task 名称,例如当返回值为 InjectTransform
时,编译后可以看到名为transformClassesWithInjectTransformForxxx 的 task。
getInputTypes
指明 Transform 处理的输入类型,在 TransformManager 中定义了很多类型:
1 | public static final Set<ScopeType> EMPTY_SCOPES = ImmutableSet.of(); |
其中,很多类型是不允许自定义 Transform 来处理的,我们常使用 CONTENT_CLASS 来操作 Class 文件。
getScopes
指明 Transform 输入文件所属的范围, 因为 gradle 是支持多工程编译的。总共有以下几种:
1 | enum Scope implements ScopeType { |
在 TransformManager 类中定义了几种范围:
1 | public static final Set<ScopeType> PROJECT_ONLY = ImmutableSet.of(Scope.PROJECT); |
常用的是SCOPE_FULL_PROJECT,代表所有Project。
确定了ContentType和Scope后就确定了该自定义Transform需要处理的资源流。比如CONTENT_CLASS和SCOPE_FULL_PROJECT表示了所有项目中java编译成的class组成的资源流。
isIncremental
指明该 Transform 是否支持增量编译。有时即使返回 true, 在某些情况下它还是会当作 false 返回。
transform
transform是一个空实现,input的内容将会打包成一个 TransformInvocation 对象。
TransformInvocation
看一下这个接口的定义:
1 | public interface TransformInvocation { |
TransformInput
1 | public interface TransformInput { |
TransformOutputProvider
1 | public interface TransformOutputProvider { |
示例:注入代码
1.首先创建一个普通的Android工程。
2.自定义Gradle插件,示例采用buildSrc方式。
关于自定义 Gradle 插件的三种方式可以参考 自定义gradle插件。
build.gradle内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15apply plugin: 'groovy'
apply plugin: 'maven'
repositories {
google()
jcenter()
mavenCentral()
}
dependencies {
implementation gradleApi()
implementation localGroovy()
implementation 'com.android.tools.build:gradle:4.1.1'
implementation 'org.javassist:javassist:3.27.0-GA'
}Transform代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57class InjectTransform extends Transform {
private Project mProject
InjectTransform(Project project) {
this.mProject = project
}
String getName() {
return "InjectTransform"
}
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental() {
return false
}
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
transformInvocation.inputs.each { input ->
// 包含我们手写的 Class 类及 R.class、BuildConfig.class 等
input.directoryInputs.each { directoryInput ->
String path = directoryInput.file.absolutePath
println("[InjectTransform] Begin to inject: $path")
// 执行注入逻辑
InjectByJavassit.inject(path, mProject)
// 获取输出目录
def dest = transformInvocation.outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
println("[InjectTransform] Directory output dest: $dest.absolutePath")
// 将input的目录复制到output指定目录
FileUtils.copyDirectory(directoryInput.file, dest)
}
// jar文件,如第三方依赖
input.jarInputs.each { jarInput ->
def dest = transformInvocation.outputProvider.getContentLocation(jarInput.name,
jarInput.contentTypes, jarInput.scopes, Format.JAR)
FileUtils.copyFile(jarInput.file, dest)
}
}
}
}Javassit代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48class InjectByJavassit {
static void inject(String path, Project project) {
try {
File dir = new File(path)
if (dir.isDirectory()) {
dir.eachFileRecurse { File file ->
if (file.name.endsWith('Activity.class')) {
doInject(project, file, path)
}
}
}
} catch (Exception e) {
e.printStackTrace()
}
}
private static void doInject(Project project, File clsFile, String originPath) {
println("[Inject] DoInject: $clsFile.absolutePath")
String cls = new File(originPath).relativePath(clsFile).replace('/', '.')
cls = cls.substring(0, cls.lastIndexOf('.class'))
println("[Inject] Cls: $cls")
ClassPool pool = ClassPool.getDefault()
// 加入当前路径
pool.appendClassPath(originPath)
// project.android.bootClasspath 加入android.jar,不然找不到android相关的所有类
pool.appendClassPath(project.android.bootClasspath[0].toString())
// 引入android.os.Bundle包,因为onCreate方法参数有Bundle
pool.importPackage('android.os.Bundle')
CtClass ctClass = pool.getCtClass(cls)
// 解冻
if (ctClass.isFrozen()) {
ctClass.defrost()
}
// 获取方法
CtMethod ctMethod = ctClass.getDeclaredMethod('onCreate')
String toastStr = 'android.widget.Toast.makeText(this, "I am the injected code", android.widget.Toast.LENGTH_SHORT).show();'
// 方法尾插入
ctMethod.insertAfter(toastStr)
ctClass.writeFile(originPath)
// 释放
ctClass.detach()
}
}注册Transform:
1
2
3
4
5
6
7class TransformPlugin implements Plugin<Project> {
void apply(Project target) {
target.android.registerTransform(new InjectTransform(target))
}
}
3.引用插件。
1 | apply plugin: com.hearing.plugin.TransformPlugin |