Transform 是什么?它解决了什么问题?
说实话,我刚接触 Android 构建时,对 Transform 这个概念也是一头雾水。你想想看,我们写代码、编译、打包,中间到底发生了什么?
Transform 是 Gradle 在 Android 构建流程中提供的一个 API。它的作用很直接——在 class 文件变成 dex 文件之前,给你一个机会去修改这些 class 文件。说白了,就是让你能在编译过程中插一脚。
我在项目中遇到过这样一个场景:我们需要给所有 Activity 自动注入埋点代码。如果手动写,几百个 Activity 改到吐。用 Transform,我只需要写一个插件,在编译时自动扫描所有 Activity 的 class 文件,插入埋点调用。嗯,这就是 Transform 的典型应用。
Transform 的工作原理
要理解 Transform,你得先知道 Android 构建的大致流程:
- Java/Kotlin 源码 → .class 文件(javac/kotlinc 编译)
- .class 文件 → Transform 链处理
- 处理后的 .class 文件 → .dex 文件(dx/d8 工具)
- .dex 文件 + 资源 → APK
Transform 就挂在第 2 步。每个 Transform 像流水线上的一个工位,拿到上一工位的输出,处理完再交给下一工位。
我画了一张图帮你理解这个流程:
创建你的第一个自定义 Transform
光说不练假把式。我们来写一个最简单的 Transform——它什么都不做,只是把输入原样传给输出。但你别小看这个骨架,我当年第一次写 Transform 时,光是搞清楚输入输出就折腾了半天。
class MyTransform : Transform() {
// Transform 的名字,在构建日志中会显示
override fun getName(): String = "MyTransform"
// 输入类型:CLASSES 表示处理 class 文件
// RESOURCES 表示处理资源文件
override fun getInputTypes(): Set<QualifiedContent.ContentType> =
setOf(QualifiedContent.DefaultContentType.CLASSES)
// 作用范围:整个项目还是只有某个变体
override fun getScopes(): Set<in QualifiedContent.Scope> =
setOf(QualifiedContent.Scope.PROJECT)
// 是否支持增量编译
override fun isIncremental(): Boolean = false
override fun transform(transformInvocation: TransformInvocation) {
// 这里就是核心逻辑
val outputProvider = transformInvocation.outputProvider
// 删除旧的输出目录
outputProvider.deleteAll()
transformInvocation.inputs.forEach { input ->
input.jarInputs.forEach { jarInput ->
// 处理 jar 包中的 class 文件
val dest = outputProvider.getContentLocation(
jarInput.name,
jarInput.contentTypes,
jarInput.scopes,
Format.JAR
)
FileUtils.copyFile(jarInput.file, dest)
}
input.directoryInputs.forEach { directoryInput ->
// 处理目录中的 class 文件
val dest = outputProvider.getContentLocation(
directoryInput.name,
directoryInput.contentTypes,
directoryInput.scopes,
Format.DIRECTORY
)
FileUtils.copyDirectory(directoryInput.file, dest)
}
}
}
}
Transform 的输入与输出
搞清楚输入输出,你就掌握了 Transform 的一半。咱们拆开来看:
输入(Inputs)
每个 Transform 的输入包含两部分:
| 输入类型 | 说明 | 常见来源 |
|---|---|---|
| JarInputs | jar 包形式的 class 文件 | 第三方库、本地 jar |
| DirectoryInputs | 目录形式的 class 文件 | 你自己写的代码编译后的 class |
你可能会问:为什么要区分 jar 和目录?嗯,这其实是为了方便处理。jar 包你直接解压处理,目录你遍历文件处理,逻辑上分开更清晰。
输出(Outputs)
输出通过 outputProvider 来管理。你需要为每个输入文件指定一个输出位置:
val dest = outputProvider.getContentLocation(
name, // 输出文件的名字
contentTypes, // 内容类型
scopes, // 作用范围
format // JAR 还是 DIRECTORY
)
然后把你处理后的文件拷贝或写入到这个位置。注意:你必须为每个输入都生成对应的输出,否则构建会报错。
增量 Transform:别再每次都全量处理了
你想想看,如果每次构建都重新处理所有 class 文件,那得有多慢?尤其是项目大了以后,改一行代码就要等几十秒。这就是增量 Transform 要解决的问题。
增量 Transform 的核心思想是:只处理发生变化的那部分文件。实现起来其实不复杂:
override fun isIncremental(): Boolean = true
override fun transform(transformInvocation: TransformInvocation) {
val outputProvider = transformInvocation.outputProvider
if (!transformInvocation.isIncremental) {
// 非增量模式:全量处理
outputProvider.deleteAll()
// ... 全量拷贝逻辑
return
}
// 增量模式:只处理变化的部分
transformInvocation.inputs.forEach { input ->
input.jarInputs.forEach { jarInput ->
when (jarInput.status) {
Status.NOTCHANGED -> {
// 文件没变,直接复用之前的输出
val dest = outputProvider.getContentLocation(
jarInput.name,
jarInput.contentTypes,
jarInput.scopes,
Format.JAR
)
// 注意:这里不需要拷贝,因为文件没变
}
Status.CHANGED, Status.ADDED -> {
// 文件变了或是新增的,需要处理
val dest = outputProvider.getContentLocation(...)
// 处理并拷贝
}
Status.REMOVED -> {
// 文件被删除了,清理对应的输出
val dest = outputProvider.getContentLocation(...)
dest.delete()
}
}
}
}
}
每个输入文件都有一个 status 属性,告诉你这个文件相对于上次构建发生了什么变化:
| 状态 | 含义 | 你的操作 |
|---|---|---|
| NOTCHANGED | 文件没变 | 跳过,复用上次输出 |
| CHANGED | 文件内容变了 | 重新处理并输出 |
| ADDED | 新增了文件 | 处理并输出 |
| REMOVED | 文件被删了 | 删除对应的输出 |
AGP 8.0 后的替代方案:Transform 被废弃了
嗯,这里有个重要的消息要告诉你。从 Android Gradle Plugin(AGP)8.0 开始,Transform API 被标记为废弃了。Google 推荐使用 AsmClassVisitorFactory 来替代。
为什么会这样?说白了,Transform API 有几个硬伤:
- 性能问题:Transform 必须处理所有 class 文件,即使你只关心其中一小部分
- 增量支持不完善:虽然可以写增量 Transform,但实现起来容易出错
- 与构建缓存不兼容:Transform 的输出很难被 Gradle 的构建缓存正确缓存
新的方案 AsmClassVisitorFactory 长这样:
abstract class MyClassVisitorFactory : AsmClassVisitorFactory<MyParameters> {
// 配置参数接口
interface MyParameters : InstrumentationParameters {
val enableLog: Property<Boolean>
}
// 判断哪些 class 需要处理
override fun isApplicable(classData: ClassData): Boolean {
// 只处理 Activity 子类
return classData.superClasses.contains("android.app.Activity")
}
// 创建 ClassVisitor 来修改字节码
override fun createClassVisitor(
classContext: ClassContext,
nextClassVisitor: ClassVisitor
): ClassVisitor {
return MyClassVisitor(nextClassVisitor)
}
}
这个新方案的好处是:
- 你只需要实现
isApplicable和createClassVisitor两个方法 - 框架自动帮你处理增量、缓存等复杂逻辑
- 性能更好,因为只处理你关心的 class
好了,这一章的内容就到这里。Transform 的原理、创建方法、输入输出、增量处理,以及未来的替代方案,我都给你讲清楚了。下一章我们会深入字节码操作,看看怎么用 ASM 框架真正地修改 class 文件。
公众号:蓝海资料掘金营,微信deep3321