Transform API是 AGP1.5 引入的概念,在AGP2.0之后并入额Android编译系统依赖的gradle-api中,主要用于构建流程 Class->Dex阶段,让我们有机会修改字节码,从而给开发者AOP 插桩提供了一种有力手段,目前AGP7.0之后TransForm又有大的变化,本文主要介绍7.0之前的使用

概述

Transform也是一种Gradle Task,主要在class-dex阶段处理 Javac编译之后的Class文件、resource文件(asset目录),本地和远程依赖的JAR/AAR。TaskManager会将每个TransformTask串联起来,前一个Transform的输出会作为下一个Transfrom的输入

Tips
每个 TransformTask 的输出都分别存储在 app/build/intermediates/transform/[Transform Name]/[Variant] 文件夹中;

Transform核心要点

注册

我们在自定义Plugin时可以通过
android.registerTransform(theTransform) 或者android.registerTransform(theTransform, dependencies).就可以进行注册(关于如何使用自定义Plugin可以看之前的文章)

1
2
3
4
5
6
class DemoPlugin: Plugin<Project> {
override fun apply(target: Project) {
val android = target.extensions.findByType(BaseExtension::class.java)
android?.registerTransform(DemoTransform())
}
}

这个过程实际上是将Transform注册到BaseExtension列表中

核心方法

我们自定义的Transform需要继承com.android.build.api.transform.Transform
传送门

主要需要处理一下几个关键方法

下面来介绍下

  • getName

transform 的名称,一个应用内可以由多个 Transform,因此需要一个名称标记,方便后面调试。

  • getInputTypes 输入内容类型

开发出给开发者使用的DefaultContentType是一个枚举,有2个

1
2
3
4
5
6
7
8
9
enum DefaultContentType implements ContentType {

// Java 字节码,包括 Jar 文件和由源码编译产生的
   CLASSES(0x01),

   // Java 资源
   RESOURCES(0x02);
}

基于资源获取的内容,关于这个可以看下 AndResGuard中的用法

AGP中还有ExtendedContentType这是给系统Transform使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 加强类型,自定义 Transform 无法使用
public enum ExtendedContentType implements ContentType {

   // DEX 文件
   DEX(0x1000),

   // Native 库
   NATIVE_LIBS(0x2000),

   // Instant Run 加强类
   CLASSES_ENHANCED(0x4000),

   // Data Binding 中间产物
   DATA_BINDING(0x10000),

   // Dex Archive
   DEX_ARCHIVE(0x40000),
;
}
  • isIncremental

是否支持增量编译;全量编译会将之前的编译产物全部删除,支持增量编译那么可以将之前的缓存利用起来节省一些编译时间和资源,这里往往需要结合文件状态一起处理

NOTCHANGED: 当前文件不需处理,甚至复制操作都不用;

ADDED、CHANGED: 正常处理,输出给下一个任务;

REMOVED: 移除outputProvider获取路径对应的文件。

  • getScopes

定义这个Transform要处理哪些输入文件,也是一个枚举类型,Scope中有

1
2
3
4
5
6
7
8
9
10
11
12
13
enum Scope implements ScopeType {

   // 当前模块
   PROJECT(0x01),
// 子模块
   SUB_PROJECTS(0x04),
// 外部依赖,包括当前模块和子模块本地依赖和远程依赖的 JAR/AAR
   EXTERNAL_LIBRARIES(0x10),
// 当前变体所测试的代码(包括依赖项)
   TESTED_CODE(0x20),
// 本地依赖和远程依赖的 JAR/AAR(provided-only)
   PROVIDED_ONLY(0x40),
}

这属于消费型输入内容,因此当前的Transfrom必须将修改后的内容复制到Transform的中间目录中,否则无法将内容传递给下一个Transform

  • transform方法

这个方法是Transform的核心方法,是我们用来进行具体转换的地方,核心入参是TransformInvocation,它提供了所有的输入输出相关信息,接口能力如下

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
public interface TransformInvocation {

/**
* transform的上下文
*/
@NonNull
Context getContext();

/**
* 返回transform的输入源
*/
@NonNull
Collection<TransformInput> getInputs();

/**
* 返回引用型输入源
*/
@NonNull Collection<TransformInput> getReferencedInputs();
/**
* 额外输入源
*/
@NonNull Collection<SecondaryInput> getSecondaryInputs();

/**
* 输出源
*/
@Nullable
TransformOutputProvider getOutputProvider();


/**
* 是否增量
*/
boolean isIncremental();
}

从上面可以看到输入源分了3类

  1. 消费型 就是需要进行transform操作的,这类对象在处理后我们必须要指定输出传给下一级
  2. 引用型 输入源指我们不进行transform操作,但可能存在查看时候使用,所以这类不需要传给下一级,再通过getReferencedScopes()指定我们的引用型输入源的作用域后, 我们可以通过TransformInvocation#getReferencedInputs()获取引用型输入源
  3. 额外定义输入源供下一级使用,开发中很少用到,不过像是ProGuardTransform中, 就会指定创建mapping.txt传给下一级; 同样像是DexMergerTransform, 如果打开了multiDex功能, 则会将maindexlist.txt文件传给下一级

利用这些输入输出的信息,我们就可以获取编译流程中的Class、Jar等文件进行操作,这个方法的核心就是遍历输入文件,再把修改后的文件复制到目标文件中,套路如下

图中选用的是asm对字节码进行操作,除此之外还有JavaAssit等方式

Transform 增量优化

从上面看来Transform用起来也不难,但是这样使用会拖慢编译时间,Transform给我们提供了增量编译,我们在覆写Transform#isIncremental接口返回true
开启增量编译模式之后,根据文件的状态做处理,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public enum Status {

// 未修改,不需要处理,也不需要复制操作
NOTCHANGED,

// 新增,正常处理并复制给下一个任务
ADDED,

// 已修改,正常处理并复制给下一个任务
CHANGED,

// 已删除,需同步移除 OutputProvider 指定的目标文件
REMOVED;
}

大致套路代码

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

@Override
public void transform(TransformInvocation transformInvocation){
Collection<TransformInput> inputs = transformInvocation.getInputs();
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
boolean isIncremental = transformInvocation.isIncremental();
//如果非增量,则清空旧的输出内容
if(!isIncremental) {
outputProvider.deleteAll();
}
for(TransformInput input : inputs) {
for(JarInput jarInput : input.getJarInputs()) {
Status status = jarInput.getStatus();
File dest = outputProvider.getContentLocation(
jarInput.getName(),
jarInput.getContentTypes(),
jarInput.getScopes(),
Format.JAR);
if(isIncremental && !emptyRun) {
switch(status) {
case NOTCHANGED:
continue;
case ADDED:
case CHANGED:
transformJar(jarInput.getFile(), dest, status);
break;
case REMOVED:
if (dest.exists()) {
FileUtils.forceDelete(dest);
}
break;
}
} else {
transformJar(jarInput.getFile(), dest, status);
}
}
for(DirectoryInput directoryInput : input.getDirectoryInputs()) {
File dest = outputProvider.getContentLocation(directoryInput.getName(),
directoryInput.getContentTypes(), directoryInput.getScopes(),
Format.DIRECTORY);
FileUtils.forceMkdir(dest);
if(isIncremental && !emptyRun) {
String srcDirPath = directoryInput.getFile().getAbsolutePath();
String destDirPath = dest.getAbsolutePath();
Map<File, Status> fileStatusMap = directoryInput.getChangedFiles();
for (Map.Entry<File, Status> changedFile : fileStatusMap.entrySet()) {
Status status = changedFile.getValue();
File inputFile = changedFile.getKey();
String destFilePath = inputFile.getAbsolutePath().replace(srcDirPath, destDirPath);
File destFile = new File(destFilePath);
switch (status) {
case NOTCHANGED:
break;
case REMOVED:
if(destFile.exists()) {
FileUtils.forceDelete(destFile);
}
break;
case ADDED:
case CHANGED:
FileUtils.touch(destFile);
transformSingleFile(inputFile, destFile, srcDirPath);
break;
}
}
} else {
transformDir(directoryInput.getFile(), dest);
}
}
}
}

除了增量编译优化之外,还可以支持并发编发,只需要将上面单个处理jar/class的逻辑,做并发处理

1
2
3
4
5
6
7
8
9
10
11
12
private WaitableExecutor waitableExecutor = WaitableExecutor.useGlobalSharedThreadPool();
//异步并发处理jar/class
waitableExecutor.execute(() -> {
bytecodeWeaver.weaveJar(srcJar, destJar);
return null;
});
waitableExecutor.execute(() -> {
bytecodeWeaver.weaveSingleClassToFile(file, outputFile, inputDirPath);
return null;
});
//等待所有任务结束
waitableExecutor.waitForTasksWithQuickFail(true);

除此之外还有一些比较好的模板代码 下面参考文中均有模板代码

关于7.0之后的变化

暂时没有时间仔细研究这个,7.0之后,Transform已经过期即将废弃,替换方式是Transform Action,和AsmClassVisitorFactory,开发起来效率比之前也高,不需要关心增量更新这些逻辑,专注于字节码的操作就可。这里就不赘述了。

小结

本文主要介绍了Transfrom使用中的一些要点,我们在业务中一些AOP场景,比如监控、Hook 三方库或者jar包中的代码等等

参考

  1. 深入理解Transform
  2. 一起玩转Android项目中的字节码
  3. Gradle Transform + ASM 探索
  4. 其实 Gradle Transform 就是个纸老虎
  5. Android Transform增量编译
  6. 现在准备好告别Transform了吗? | 拥抱AGP7.0
  7. App流畅度优化:利用字节码插桩实现一个快速排查高耗时方法的工具