ASM 是 Java 字节码操作框架,相比 AspectJ 的”声明式”切面,ASM 提供的是”命令式”的字节码级别控制。在全埋点场景中,通过 Gradle Transform + ASM 可以在编译期对所有 class 文件扫描和修改,在 onClick 方法中插入埋点指令。
Android Gradle 插件提供 Transform API,在 .class → .dex 转换阶段拦截所有 class 文件:
class TrackingPlugin : Plugin<Project> { override fun apply(project: Project) { val android = project.extensions.getByType(AppExtension::class.java) android.registerTransform(TrackingTransform()) } }
|
class TrackingTransform : Transform() { override fun getName() = "clickTracking" override fun getInputTypes() = setOf(QualifiedContent.DefaultContentType.CLASSES) override fun getScopes() = setOf(QualifiedContent.Scope.PROJECT)
override fun transform(transformInvocation: TransformInvocation) { transformInvocation.inputs.forEach { input -> input.directoryInputs.forEach { dirInput -> dirInput.file.walkTopDown() .filter { it.isFile && it.extension == "class" } .forEach { classFile -> processClass(classFile) } } input.jarInputs.forEach { jarInput -> } } } }
|
二、ASM ClassVisitor 实现
class OnClickClassVisitor( api: Int, next: ClassVisitor ) : ClassVisitor(api, next) {
private var className: String = ""
override fun visit( version: Int, access: Int, name: String, signature: String?, superName: String?, interfaces: Array<out String>? ) { className = name super.visit(version, access, name, signature, superName, interfaces) }
override fun visitMethod( access: Int, name: String, descriptor: String, signature: String?, exceptions: Array<out String>? ): MethodVisitor { val mv = super.visitMethod(access, name, descriptor, signature, exceptions)
if (name == "onClick" && descriptor == "(Landroid/view/View;)V") { return OnClickMethodVisitor(api, mv, className) } return mv } }
|
三、MethodVisitor:插入埋点代码
class OnClickMethodVisitor( api: Int, next: MethodVisitor, private val className: String ) : MethodVisitor(api, next) {
override fun visitCode() { super.visitCode()
mv.visitVarInsn(Opcodes.ALOAD, 1) mv.visitMethodInsn( Opcodes.INVOKESTATIC, "com/example/TrackingHelper", "trackClick", "(Landroid/view/View;)V", false ) } }
|
四、处理不同签名
实际场景中 OnClickListener 可能有多种形态:
通过检查类实现的接口来判断:
override fun visit(..., interfaces: Array<out String>?) { val isOnClickListener = interfaces?.contains("android/view/View\$OnClickListener") == true }
|
五、ASM vs AspectJ 深度对比
| 维度 |
ASM |
AspectJ |
| API 层级 |
字节码指令级 |
源码语义级 |
| 学习曲线 |
陡峭(需理解 JVM 指令) |
平缓(Java/Kotlin 语法) |
| 灵活性 |
极高(可修改任意字节码) |
受 AOP 模型约束 |
| 性能 |
编译期稍慢(字节码量大) |
与 ASM 相近 |
| 维护性 |
差(版本升级需改动指令) |
好(Pointcut 表达式稳定) |
面试常考问题
Q1:Gradle Transform 为什么在 AGP 7.0+ 被废弃?
AGP 7.0+ 引入了新的 Artifacts Transform API 和 AsmClassVisitorFactory,原因是旧的 Transform API 顺序不透明、增量编译支持不佳。新 API 支持增量编译与缓存,提升构建速度。旧 Transform 在 AGP 8.0 中完全移除。全埋点方案需适配新 API:AsmClassVisitorFactory.registerInstrumentation()。
Q2:ASM 方案如何处理混淆(ProGuard/R8)后的类?
Gradle Transform 的执行在混淆之前,因此处理的仍是未混淆的 class 文件,类名和方法签名都是原始名称。埋点辅助类 TrackingHelper 需要在混淆规则中 keep 住,否则调用指令 INVOKESTATIC 的目标类名发生变化会导致运行时 NoClassDefFoundError:
-keep class com.example.TrackingHelper { *; }
|
Q3:ASM 方法插桩会导致方法帧栈(Stack Frame)问题吗?
会。ASM 在插入新指令后,方法的 Stack Map Frame 会变化。如果使用 COMPUTE_FRAMES(ClassWriter.COMPUTE_FRAMES),ASM 会自动重新计算栈帧,但会使转换速度变慢。也可以手动使用 visitFrame() 维护,但在复杂方法中容易出错。官方推荐使用 COMPUTE_FRAMES 以简化实现。