目录
  1. 1. 一、Gradle Transform 注册
  2. 2. 二、ASM ClassVisitor 实现
  3. 3. 三、MethodVisitor:插入埋点代码
  4. 4. 四、处理不同签名
  5. 5. 五、ASM vs AspectJ 深度对比
  6. 6. 面试常考问题
【全埋点方案系列】AppClick全埋点之ASM处理

ASM 是 Java 字节码操作框架,相比 AspectJ 的”声明式”切面,ASM 提供的是”命令式”的字节码级别控制。在全埋点场景中,通过 Gradle Transform + ASM 可以在编译期对所有 class 文件扫描和修改,在 onClick 方法中插入埋点指令。

一、Gradle Transform 注册

Android Gradle 插件提供 Transform API,在 .class.dex 转换阶段拦截所有 class 文件:

// buildSrc 或独立插件模块
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 ->
// 处理 jar 包中的 class
}
}
}
}

二、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)

// 只处理 onClick(View) 方法
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()
// 在方法开头插入埋点调用
// 等价于:TrackingHelper.trackClick($1) —— $1 是方法第一个参数(View)

mv.visitVarInsn(Opcodes.ALOAD, 1) // 加载参数 View
mv.visitMethodInsn(
Opcodes.INVOKESTATIC,
"com/example/TrackingHelper", // 埋点辅助类
"trackClick",
"(Landroid/view/View;)V",
false
)
}
}

四、处理不同签名

实际场景中 OnClickListener 可能有多种形态:

// 标准接口: void onClick(View v)   →  descriptor: (Landroid/view/View;)V
// Kotlin lambda: void onClick(View v) → 相同 descriptor
// 自定义内部类可能有不同的方法名

// ASM 中需同时匹配:
// - android/view/View$OnClickListener
// - android/content/DialogInterface$OnClickListener
// - android/widget/AdapterView$OnItemClickListener

通过检查类实现的接口来判断:

override fun visit(..., interfaces: Array<out String>?) {
val isOnClickListener = interfaces?.contains("android/view/View\$OnClickListener") == true
// 记录状态,在 visitMethod 时决定是否植入
}

五、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_FRAMESClassWriter.COMPUTE_FRAMES),ASM 会自动重新计算栈帧,但会使转换速度变慢。也可以手动使用 visitFrame() 维护,但在复杂方法中容易出错。官方推荐使用 COMPUTE_FRAMES 以简化实现。

打赏
  • 微信
  • 支付宝

评论