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 API 其实没那么复杂。核心就是三件事:
- 实现 AsmClassVisitorFactory:告诉框架你想处理哪些类,以及怎么处理
- 写 ASM 代码:用 ClassVisitor 和 MethodVisitor 做字节码插桩
- 配好参数注解:让 Gradle 知道什么时候该增量,什么时候该全量
我在项目里用这套方案替换了旧的 Transform 后,构建时间从 2 分钟降到了 40 秒。效果还是很明显的。如果你还在用旧 API,我建议尽早迁移过来。
一句话总结:AGP 7+ 的 Transform 新方案,性能更好、增量更智能、配置更简单。值得花时间掌握。
公众号:蓝海资料掘金营,微信deep3321