Transform 是什么?它解决了什么问题?

说实话,我刚接触 Android 构建时,对 Transform 这个概念也是一头雾水。你想想看,我们写代码、编译、打包,中间到底发生了什么?

Transform 是 Gradle 在 Android 构建流程中提供的一个 API。它的作用很直接——在 class 文件变成 dex 文件之前,给你一个机会去修改这些 class 文件。说白了,就是让你能在编译过程中插一脚。

我在项目中遇到过这样一个场景:我们需要给所有 Activity 自动注入埋点代码。如果手动写,几百个 Activity 改到吐。用 Transform,我只需要写一个插件,在编译时自动扫描所有 Activity 的 class 文件,插入埋点调用。嗯,这就是 Transform 的典型应用。

核心作用:Transform 让你在 .class → .dex 的转换过程中,对字节码进行读取、修改、替换或删除。

Transform 的工作原理

要理解 Transform,你得先知道 Android 构建的大致流程:

  1. Java/Kotlin 源码 → .class 文件(javac/kotlinc 编译)
  2. .class 文件 → Transform 链处理
  3. 处理后的 .class 文件 → .dex 文件(dx/d8 工具)
  4. .dex 文件 + 资源 → APK

Transform 就挂在第 2 步。每个 Transform 像流水线上的一个工位,拿到上一工位的输出,处理完再交给下一工位。

我画了一张图帮你理解这个流程:

Transform 在 Android 构建流程中的位置 Java/Kotlin 源码 javac / kotlinc 编译 Transform 链(你的插件在这里) Transform A → Transform B → Transform C 读取 .class → 修改字节码 → 输出 .class Dex 文件 d8 / dx 工具 你只能在这个阶段修改字节码 一旦变成 dex,就改不了了 💡 Transform 的输入:.class 文件 💡 Transform 的输出:修改后的 .class 文件

创建你的第一个自定义 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 是否被正确注册和调用。

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
)

然后把你处理后的文件拷贝或写入到这个位置。注意:你必须为每个输入都生成对应的输出,否则构建会报错。

我曾经踩过的坑:有一次我忘了处理某个 jarInput,结果构建出来的 APK 缺少了那个库的代码,运行时直接 ClassNotFoundException。排查了半天才发现是 Transform 里漏了拷贝。所以,记得遍历所有输入!

增量 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 文件被删了 删除对应的输出
我的建议:增量 Transform 能大幅提升构建速度。我在一个中型项目上测试过,开启增量后,二次构建时间从 15 秒降到了 3 秒。但要注意,如果你的 Transform 逻辑依赖全局状态(比如统计所有类的数量),增量模式下可能会出问题,需要额外处理。

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)
    }
}

这个新方案的好处是:

  • 你只需要实现 isApplicablecreateClassVisitor 两个方法
  • 框架自动帮你处理增量、缓存等复杂逻辑
  • 性能更好,因为只处理你关心的 class
注意:如果你还在用 AGP 7.x 或更低版本,Transform API 仍然可以正常使用。但如果你要写新的插件,我建议直接使用 AsmClassVisitorFactory。毕竟,迟早要迁移的,早学早省心。

好了,这一章的内容就到这里。Transform 的原理、创建方法、输入输出、增量处理,以及未来的替代方案,我都给你讲清楚了。下一章我们会深入字节码操作,看看怎么用 ASM 框架真正地修改 class 文件。


公众号:蓝海资料掘金营,微信deep3321