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] 文件夹中;
注册
我们在自定义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
传送门
主要需要处理一下几个关键方法

下面来介绍下
transform 的名称,一个应用内可以由多个 Transform,因此需要一个名称标记,方便后面调试。
开发出给开发者使用的DefaultContentType是一个枚举,有2个
1 2 3 4 5 6 7 8 9
| enum DefaultContentType implements ContentType { CLASSES(0x01), 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
| public enum ExtendedContentType implements ContentType { DEX(0x1000), NATIVE_LIBS(0x2000), CLASSES_ENHANCED(0x4000), DATA_BINDING(0x10000), DEX_ARCHIVE(0x40000), ; }
|
是否支持增量编译;全量编译会将之前的编译产物全部删除,支持增量编译那么可以将之前的缓存利用起来节省一些编译时间和资源,这里往往需要结合文件状态一起处理
NOTCHANGED: 当前文件不需处理,甚至复制操作都不用;
ADDED、CHANGED: 正常处理,输出给下一个任务;
REMOVED: 移除outputProvider获取路径对应的文件。
定义这个Transform要处理哪些输入文件,也是一个枚举类型,Scope中有
1 2 3 4 5 6 7 8 9 10 11 12 13
| enum Scope implements ScopeType { PROJECT(0x01), SUB_PROJECTS(0x04), EXTERNAL_LIBRARIES(0x10), TESTED_CODE(0x20), PROVIDED_ONLY(0x40), }
|
这属于消费型输入内容,因此当前的Transfrom必须将修改后的内容复制到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 {
@NonNull Context getContext();
@NonNull Collection<TransformInput> getInputs();
@NonNull Collection<TransformInput> getReferencedInputs();
@NonNull Collection<SecondaryInput> getSecondaryInputs();
@Nullable TransformOutputProvider getOutputProvider();
boolean isIncremental(); }
|
从上面可以看到输入源分了3类
- 消费型 就是需要进行transform操作的,这类对象在处理后我们必须要指定输出传给下一级
- 引用型 输入源指我们不进行transform操作,但可能存在查看时候使用,所以这类不需要传给下一级,再通过getReferencedScopes()指定我们的引用型输入源的作用域后, 我们可以通过TransformInvocation#getReferencedInputs()获取引用型输入源
- 额外定义输入源供下一级使用,开发中很少用到,不过像是ProGuardTransform中, 就会指定创建mapping.txt传给下一级; 同样像是DexMergerTransform, 如果打开了multiDex功能, 则会将maindexlist.txt文件传给下一级
利用这些输入输出的信息,我们就可以获取编译流程中的Class、Jar等文件进行操作,这个方法的核心就是遍历输入文件,再把修改后的文件复制到目标文件中,套路如下

图中选用的是asm对字节码进行操作,除此之外还有JavaAssit等方式
从上面看来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, 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();
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包中的代码等等
参考
- 深入理解Transform
- 一起玩转Android项目中的字节码
- Gradle Transform + ASM 探索
- 其实 Gradle Transform 就是个纸老虎
- Android Transform增量编译
- 现在准备好告别Transform了吗? | 拥抱AGP7.0
- App流畅度优化:利用字节码插桩实现一个快速排查高耗时方法的工具