ASM 是 Java 字节码操作框架,相比 AspectJ 的”声明式”切面,ASM 提供的是”命令式”的字节码级别控制。在全埋点场景中,通过 Gradle Transform + ASM 可以在编译期对所有 class 文件扫描和修改,在 onClick 方法中插入埋点指令。
一、ASM 框架架构
1.1 核心设计模式:Visitor 模式
ClassReader (读取) ↓ ClassVisitor (访问/修改类结构) ├── visit(version, access, name, signature, superName, interfaces) ├── visitField(...) → FieldVisitor │ ├── visitAnnotation(...) │ └── visitEnd() ├── visitMethod(...) → MethodVisitor │ ├── visitCode() │ ├── visitFrame(...) │ ├── visitVarInsn(...) │ ├── visitMethodInsn(...) │ ├── visitFieldInsn(...) │ ├── visitJumpInsn(...) │ ├── visitLdcInsn(...) │ └── visitEnd() └── visitEnd() ↓ ClassWriter (生成) └── toByteArray()
|
1.2 Opcodes 常量速查
object AsmOpcodes { const val ALOAD = Opcodes.ALOAD const val ASTORE = Opcodes.ASTORE const val DUP = Opcodes.DUP const val POP = Opcodes.POP const val RETURN = Opcodes.RETURN const val ARETURN = Opcodes.ARETURN const val ACONST_NULL = Opcodes.ACONST_NULL
const val INVOKESTATIC = Opcodes.INVOKESTATIC const val INVOKEVIRTUAL = Opcodes.INVOKEVIRTUAL const val INVOKEINTERFACE = Opcodes.INVOKEINTERFACE
const val GETSTATIC = Opcodes.GETSTATIC const val PUTSTATIC = Opcodes.PUTSTATIC const val GETFIELD = Opcodes.GETFIELD const val PUTFIELD = Opcodes.PUTFIELD
const val CHECKCAST = Opcodes.CHECKCAST const val NEW = Opcodes.NEW }
|
二、AGP 8.0 兼容方案:AsmClassVisitorFactory
2.1 注册工厂
abstract class TrackingClassVisitorFactory : AsmClassVisitorFactory<InstrumentationParameters.None> {
override fun createClassVisitor( classContext: ClassContext, nextClassVisitor: ClassVisitor ): ClassVisitor { return TrackingClassVisitor( Opcodes.ASM9, nextClassVisitor, classContext.currentClassData.className ) }
override fun isInstrumentable(classData: ClassData): Boolean { return !classData.className.startsWith("android.") && !classData.className.startsWith("androidx.") && !classData.className.startsWith("com.google.") && !classData.className.contains(".R\$") && classData.className != "com.example.app.R" && classData.className != "com.example.app.BuildConfig" && classData.className.contains("com.example.app") } }
|
2.2 Gradle Plugin 注册
class TrackingPlugin : Plugin<Project> {
override fun apply(project: Project) { val androidComponents = project.extensions .getByType(AndroidComponentsExtension::class.java)
androidComponents.onVariants { variant -> variant.instrumentation.transformClassesWith( TrackingClassVisitorFactory::class.java, InstrumentationScope.ALL ) { }
variant.instrumentation.setAsmFramesComputationMode( FramesComputationMode.COMPUTE_FRAMES ) } } }
|
三、ClassVisitor 实现
class TrackingClassVisitor( api: Int, next: ClassVisitor, private val className: String ) : ClassVisitor(api, next) {
private var isOnClickListener = false
private var hasOnClickMethod = false
private var interfaces: Array<out String>? = null
override fun visit( version: Int, access: Int, name: String, signature: String?, superName: String?, interfaces: Array<out String>? ) { this.interfaces = interfaces
isOnClickListener = interfaces?.contains("android/view/View\$OnClickListener") == true
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 (isOnClickListener && name == "onClick" && descriptor == "(Landroid/view/View;)V") { return OnClickMethodVisitor(api, mv, className, access, name, descriptor) }
if (name == "onCreate" || name == "onViewCreated" || name == "onBindViewHolder") { return SetOnClickListenerScanner(api, mv, className, access, name, descriptor) }
return mv } }
|
四、MethodVisitor:onClick 方法插桩
class OnClickMethodVisitor( api: Int, next: MethodVisitor, private val className: String, private val methodAccess: Int, private val methodName: String, private val methodDesc: String ) : MethodVisitor(api, next) {
private val viewParamIndex = findViewParamIndex(methodDesc)
override fun visitCode() {
mv.visitVarInsn(Opcodes.ALOAD, viewParamIndex)
mv.visitMethodInsn( Opcodes.INVOKESTATIC, "com/example/tracking/TrackingHelper", "trackClick", "(Landroid/view/View;)V", false )
super.visitCode() }
private fun findViewParamIndex(desc: String): Int { val isStatic = (methodAccess and Opcodes.ACC_STATIC) != 0 var index = if (isStatic) 0 else 1
val args = desc.substring(1, desc.lastIndexOf(')')) var i = 0 while (i < args.length) { when { args[i] == 'L' -> { val end = args.indexOf(';', i) val type = args.substring(i + 1, end) if (type == "android/view/View") { return index } index++ i = end + 1 } args[i] == '[' -> { i++ continue } else -> { if (args[i] == 'D' || args[i] == 'J') index++ index++ i++ } } } return 1 }
override fun visitMaxs(maxStack: Int, maxLocals: Int) { super.visitMaxs(maxOf(maxStack, 1), maxLocals) } }
|
五、MethodVisitor:setOnClickListener 调用扫描
class SetOnClickListenerScanner( api: Int, next: MethodVisitor, private val className: String, access: Int, name: String, desc: String ) : MethodVisitor(api, next) {
override fun visitMethodInsn( opcode: Int, owner: String, name: String, descriptor: String, isInterface: Boolean ) { if (name == "setOnClickListener" && (owner == "android/view/View" || owner == "android/view/ViewGroup") && descriptor == "(Landroid/view/View\$OnClickListener;)V" ) {
mv.visitMethodInsn( Opcodes.INVOKESTATIC, "com/example/tracking/TrackingHelper", "wrapOnClickListener", "(Landroid/view/View\$OnClickListener;)Landroid/view/View\$OnClickListener;", false ) }
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface) } }
|
5.1 wrapOnClickListener 辅助方法
object TrackingHelper {
@JvmStatic fun wrapOnClickListener(origin: View.OnClickListener): View.OnClickListener { if (origin is TrackingOnClickListener) return origin
return TrackingOnClickListener(origin) }
@JvmStatic fun trackClick(view: View) { val info = buildMap { put("view_class", view.javaClass.name) put("view_id", view.id) put("view_text", ((view as? TextView)?.text?.toString() ?: "").take(100)) put("page_name", getPageName(view.context)) put("timestamp", System.currentTimeMillis()) put("track_method", "asm") } AnalyticsSDK.track("app_click", info) }
@JvmStatic fun trackClickError(view: View, error: Throwable) { AnalyticsSDK.track("click_error", mapOf( "view_class" to view.javaClass.name, "error" to error.javaClass.name, "message" to (error.message ?: "") )) }
private fun getPageName(context: Context): String { var ctx = context while (ctx is ContextWrapper) { if (ctx is Activity) return ctx.javaClass.simpleName ctx = ctx.baseContext } return context.javaClass.simpleName } }
class TrackingOnClickListener( private val origin: View.OnClickListener ) : View.OnClickListener {
override fun onClick(v: View) { TrackingHelper.trackClick(v)
try { origin.onClick(v) } catch (e: Exception) { TrackingHelper.trackClickError(v, e) throw e } } }
|
六、栈帧(Stack Frame)计算
6.1 COMPUTE_FRAMES 模式
class TrackingClassWriter : ClassWriter(ClassWriter.COMPUTE_FRAMES) { override fun getClassLoader(): ClassLoader { return Thread.currentThread().contextClassLoader ?: super.getClassLoader() } }
|
6.2 帧计算性能影响
模式 | 构建速度 | 正确性 | 推荐场景 ---------------|----------|--------|--------- COMPUTE_MAXS | 100% | 低风险 | 只插入简单调用(不改变控制流) COMPUTE_FRAMES | 50-70% | 高 | 插入了跳转/异常处理/新局部变量 手动 visitFrame | 最快 | 易出错 | 对性能极端敏感的 Transform
|
@Deprecated("Use AsmClassVisitorFactory for AGP 8.0+") class LegacyAsmTransform : Transform() {
override fun getName() = "asmTracking"
override fun transform(invocation: TransformInvocation) { invocation.inputs.forEach { input -> input.directoryInputs.forEach { dirInput -> processDirectory(dirInput) } input.jarInputs.forEach { jarInput -> processJar(jarInput) } } }
private fun processDirectory(dirInput: DirectoryInput) { dirInput.file.walkTopDown() .filter { it.extension == "class" } .forEach { classFile -> val reader = ClassReader(classFile.readBytes()) val writer = ClassWriter(reader, ClassWriter.COMPUTE_FRAMES) val visitor = TrackingClassVisitor(Opcodes.ASM9, writer, "") reader.accept(visitor, ClassReader.EXPAND_FRAMES) classFile.writeBytes(writer.toByteArray()) } } }
|
八、构建性能优化
优化策略 | 效果 ------------------------------------|--------- 限定扫描范围 (isInstrumentable) | 过滤掉 70-80% 的不相关类 使用 ClassReader.SKIP_FRAMES | 跳过帧处理,速度提升 20% 使用 ClassReader.SKIP_DEBUG | 跳过调试信息,速度提升 10% 使用 ClassReader.SKIP_CODE | 跳过方法体(仅扫描签名时用) 共享 ClassReader 实例 | 减少对象创建 缓存 className 判断结果 | 同项目多次构建复用
|
九、ProGuard/R8 规则
# 保持 ASM 插桩调用的目标方法 -keep class com.example.tracking.TrackingHelper { public static void trackClick(android.view.View); public static void trackClickError(android.view.View, java.lang.Throwable); public static android.view.View$OnClickListener wrapOnClickListener(android.view.View$OnClickListener); }
# 保持包装后的 OnClickListener 不被内联 -keep class com.example.tracking.TrackingOnClickListener { public void onClick(android.view.View); }
# 防止 R8 移除未被直接引用的 onClick 实现 -keepclassmembers class * { public void onClick(android.view.View); }
# ASM 库 -dontwarn org.objectweb.asm.** -keep class org.objectweb.asm.** { *; }
|
十、完整项目结构
buildSrc/ └── src/main/kotlin/com/example/tracking/plugin/ ├── TrackingPlugin.kt # Gradle Plugin 入口 ├── TrackingClassVisitorFactory.kt # AGP 8.0+ Instrumentation └── visitors/ ├── TrackingClassVisitor.kt # Class 级访问器 ├── OnClickMethodVisitor.kt # onClick 方法插桩 └── SetOnClickListenerScanner.kt # setOnClickListener 扫描
app/ └── src/main/java/com/example/tracking/ └── TrackingHelper.kt # 埋点辅助类(被 ASM 调用的目标)
app/build.gradle.kts # 应用插件
|
面试常考问题
Q1:Gradle Transform 为什么在 AGP 7.0+ 被废弃?
AGP 7.0+ 引入了新的 Artifacts Transform API 和 AsmClassVisitorFactory,原因是旧的 Transform API 存在根本性缺陷:(1)顺序不透明——多个 Transform 的执行顺序由注册顺序决定,开发者难以控制,容易产生冲突;(2)增量编译支持不佳——旧 API 的 isIncremental() 在复杂场景下可靠性差;(3)性能瓶颈——Transform 处理所有 class 文件,包括已经过 ProGuard/R8 处理的依赖库 class,导致重复工作;(4)缓存失效频繁——任何项目的 class 变更都会触发整个 Transform 重新执行。新 API 的 AsmClassVisitorFactory 支持增量编译、更好的缓存策略、更明确的作用域(PROJECT / ALL),且作为 AGP 的一等公民持续得到优化。旧 Transform 在 AGP 8.0 中完全移除。
Q2:ASM 方案如何处理混淆(ProGuard/R8)后的类?
Gradle Transform 的执行在 ProGuard/R8 之前,因此处理的仍是未混淆的 class 文件,类名和方法签名都是原始名称。但埋点辅助类 TrackingHelper 需要在混淆规则中 keep 住,否则调用指令 INVOKESTATIC com/example/tracking/TrackingHelper.trackClick(View)V 中的目标类名在混淆后发生变化,运行时找不到方法会抛出 NoClassDefFoundError 或 NoSuchMethodError。ProGuard 规则:-keep class com.example.tracking.TrackingHelper { *; }。此外,如果 R8 开启了 fullMode,可能直接内联 TrackingHelper.trackClick 到每个调用点,这种情况下反而是安全的(内联后不存在外部符号引用)。
Q3:ASM 方法插桩会导致方法帧栈(Stack Frame)问题吗?
会。Stack Map Frame 是 Java 6+ class 文件中用于字节码验证的元数据,记录了每条指令执行时操作数栈和局部变量表的类型信息。当 ASM 在现有方法中插入新指令(如 ALOAD + INVOKESTATIC),栈帧可能发生变化:(1)如果使用 COMPUTE_FRAMES(ClassWriter.COMPUTE_FRAMES),ASM 会自动重新计算所有栈帧,正确性最高但使转换速度慢约 1.5-2 倍;(2)COMPUTE_MAXS 只计算栈最大深度,不重新计算帧类型,适用于简单的指令插入(不改变控制流和局部变量表布局);(3)手动使用 visitFrame() 维护帧信息最快但最容易出错,需要对 JVM 规范和当前方法的控制流有精确理解。官方推荐在复杂插入场景使用 COMPUTE_FRAMES,在简单调用插入(如本文的 onClick 开头插入)使用 COMPUTE_MAXS。
Q4:ASM 如何区分 OnClickListener 的不同实现方式(显式 implements、匿名内部类、Kotlin lambda、方法引用)?
所有方式编译后都会在字节码中实现 View$OnClickListener 接口,ASM 通过检查类实现的接口列表来统一识别。不同实现方式的区别仅在类名和所在位置:(1)显式 implements View.OnClickListener:类名正常,在 visitClass 中直接匹配接口;(2)匿名内部类:类名形如 MainActivity$1,通过 outerClass 关联到所在 Activity;(3)Kotlin lambda:编译后同样生成匿名内部类,ASM 无法在字节码层面区分 Java lambda 和 Kotlin lambda;(4)方法引用 this::onClick:编译后生成 invokedynamic 指令,在 Android 上会被脱糖为匿名内部类。ASM 不在意实现细节,只要字节码层面有 onClick(View)V 方法并被标记为 implements OnClickListener,就能正确插桩。
Q5:在 AGP 8.0 的新 API 中,如何处理 Jar 中的第三方库 class?
AsmClassVisitorFactory 的 InstrumentationScope.ALL 允许处理所有 class 文件(包括 jar/aar 中的),InstrumentationScope.PROJECT 只处理项目自身的源码编译产物。处理第三方库的 class 时,有以下注意事项:(1)对系统库(android.*、androidx.*)插桩可能导致崩溃,应在 isInstrumentable() 中排除;(2)第三方库的混淆机制不同,需要确保 TrackingHelper 在所有 dex 的分包中都能被访问(使用 multidex 的 keep 规则);(3)修改第三方库的 class 可能违反该库的许可协议(如 GPL 的传染性),需要法律审查。