23、自定义Transform(AGP 7+):Transform API变迁、AsmClassVisitorFactory、字节码插桩、增量Transform、兼容性处理

说实话,Transform API 在 AGP 7 之后经历了一次大换血。我最早接触它的时候还是 AGP 3.x 版本,那时候写个 Transform 还挺爽的,直接继承一个类就完事了。但到了 AGP 7+,官方把旧的 Transform API 标记为废弃,然后推出了新的方案。嗯,今天我们就来聊聊这个变迁,以及怎么在新的体系下做字节码插桩。

Transform API 为什么会被废弃?

你想想看,旧的 Transform API 有什么问题?

  • 性能问题:每次构建都要全量处理,增量支持形同虚设
  • API 设计不合理:直接暴露了太多底层细节
  • 与 Gradle 配置缓存不兼容:导致构建缓存失效
  • 维护成本高:Google 自己都懒得修了

我在项目中遇到过好几次,因为 Transform 导致构建时间从 30 秒暴涨到 2 分钟。后来一查,发现是某个第三方库的 Transform 没有做增量处理,每次都在全量扫描 class 文件。那叫一个痛苦。

核心变化:AGP 7+ 推荐使用 AsmClassVisitorFactory 替代旧的 Transform API。新的方案基于 ASM 框架,并且天然支持增量构建和配置缓存。

AsmClassVisitorFactory 是什么?

说白了,这是一个工厂类,用来创建 ASM 的 ClassVisitor。你只需要实现一个接口,告诉框架你想处理哪些 class 文件,然后框架会在合适的时机调用你的 visitor。

它的核心接口长这样:

interface AsmClassVisitorFactory<ParametersT : InstrumentationParameters> {
    fun createClassVisitor(
        classContext: ClassContext,
        nextClassVisitor: ClassVisitor
    ): ClassVisitor

    fun isInstrumentable(classData: ClassData): Boolean
}

我个人习惯把 isInstrumentable 当作一个过滤器。比如我只想处理 Activity 的子类,就在这里做判断。这样框架就不会把无关的 class 文件传进来,性能会好很多。

实战:写一个字节码插桩插件

我们来做一个实际例子:给所有 Activity 的 onCreate 方法插入一行日志。这样每次 Activity 创建时,我们都能知道它被调用了。

第一步,定义参数:

abstract class LogInstrumentationParameters : InstrumentationParameters {
    @get:Input
    abstract val logTag: Property<String>
}

第二步,实现工厂类:

abstract class LogAsmClassVisitorFactory :
    AsmClassVisitorFactory<LogInstrumentationParameters> {

    override fun isInstrumentable(classData: ClassData): Boolean {
        // 只处理 Activity 的子类
        return classData.superClasses.contains("android.app.Activity")
    }

    override fun createClassVisitor(
        classContext: ClassContext,
        nextClassVisitor: ClassVisitor
    ): ClassVisitor {
        return LogClassVisitor(nextClassVisitor, parameters.get().logTag)
    }
}

第三步,写 ASM 的 ClassVisitor:

class LogClassVisitor(
    cv: ClassVisitor,
    private val logTag: String
) : ClassVisitor(Opcodes.ASM9, cv) {

    override fun visitMethod(
        access: Int,
        name: String,
        descriptor: String,
        signature: String?,
        exceptions: Array<String>?
    ): MethodVisitor {
        val mv = super.visitMethod(access, name, descriptor, signature, exceptions)
        if (name == "onCreate" && descriptor == "(Landroid/os/Bundle;)V") {
            return LogMethodVisitor(mv, logTag)
        }
        return mv
    }
}

class LogMethodVisitor(
    mv: MethodVisitor,
    private val logTag: String
) : MethodVisitor(Opcodes.ASM9, mv) {

    override fun visitCode() {
        super.visitCode()
        mv.visitLdcInsn(logTag)
        mv.visitLdcInsn("onCreate called")
        mv.visitMethodInsn(
            Opcodes.INVOKESTATIC,
            "android/util/Log",
            "d",
            "(Ljava/lang/String;Ljava/lang/String;)I",
            false
        )
        mv.visitInsn(Opcodes.POP)
    }
}

小提示:ASM 的版本号要注意。AGP 7+ 内部用的是 ASM 9,所以你在写 ClassVisitor 时也要用 Opcodes.ASM9。版本不匹配会导致运行时异常。

增量 Transform 是怎么实现的?

旧的 Transform API 虽然也有增量模式,但实现起来很麻烦。你需要自己维护一个状态文件,记录哪些文件发生了变化。新的 AsmClassVisitorFactory 直接帮你搞定了。

框架会通过 Gradle 的输入变更检测,自动判断哪些 class 文件需要重新处理。你只需要在参数上加上 @Input@InputFile 注解,剩下的交给 Gradle。

举个例子:

abstract class MyParams : InstrumentationParameters {
    @get:Input
    abstract val enableLog: Property<Boolean>

    @get:InputFile
    @get:PathSensitive(PathSensitivity.RELATIVE)
    abstract val configFile: RegularFileProperty
}

enableLog 的值变了,或者 configFile 的内容变了,Gradle 就会重新执行这个 Transform。否则,它会直接复用上次的缓存结果。

我曾经踩过一个坑:忘记给参数加 @Input 注解,结果改了配置后构建还是用的旧缓存。排查了半天才发现是注解漏了。嗯,这个细节一定要注意。

兼容性处理:新旧 API 共存

如果你的项目还在用 AGP 7 以下的版本,但又想用新的 API,怎么办?

我的做法是写一个兼容层。在 Gradle 插件中判断 AGP 版本,然后动态选择使用旧 Transform 还是新 API。

val agpVersion = AndroidProject.DEFAULT_BUILD_TOOLS_VERSION
if (agpVersion.startsWith("7.") || agpVersion.startsWith("8.")) {
    // 使用 AsmClassVisitorFactory
    project.extensions.configure<AndroidComponentsExtension> {
        onVariants { variant ->
            variant.transformClassesWith(
                LogAsmClassVisitorFactory::class.java,
                InstrumentationScope.ALL
            ) { params ->
                params.logTag.set("MyApp")
            }
        }
    }
} else {
    // 使用旧的 Transform API
    project.extensions.getByType<BaseAppModuleExtension>()
        .registerTransform(OldLogTransform())
}

注意transformClassesWith 方法是在 AGP 7.0 才引入的。如果你在 AGP 4.x 上调用它,会直接编译报错。所以版本判断一定要做在前面。

知识体系总览

下面这张图总结了本章的核心内容,从 API 变迁到具体实现,一目了然:

自定义Transform知识体系 Transform API 变迁 AGP 3.x ~ 6.x:旧Transform(废弃) → AGP 7+:AsmClassVisitorFactory(推荐) AsmClassVisitorFactory 核心组件 isInstrumentable() createClassVisitor() InstrumentationParameters 字节码插桩 ASM ClassVisitor + MethodVisitor 增量Transform @Input / @InputFile 注解 兼容性处理 AGP版本判断 + 兼容层 最佳实践:参数注解 → 增量缓存 → 版本兼容 → 性能优化

总结一下

新的 Transform API 其实没那么复杂。核心就是三件事:

  1. 实现 AsmClassVisitorFactory:告诉框架你想处理哪些类,以及怎么处理
  2. 写 ASM 代码:用 ClassVisitor 和 MethodVisitor 做字节码插桩
  3. 配好参数注解:让 Gradle 知道什么时候该增量,什么时候该全量

我在项目里用这套方案替换了旧的 Transform 后,构建时间从 2 分钟降到了 40 秒。效果还是很明显的。如果你还在用旧 API,我建议尽早迁移过来。

一句话总结:AGP 7+ 的 Transform 新方案,性能更好、增量更智能、配置更简单。值得花时间掌握。


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