目录
  1. 1. 一、ASM 框架架构
    1. 1.1. 1.1 核心设计模式:Visitor 模式
    2. 1.2. 1.2 Opcodes 常量速查
  2. 2. 二、AGP 8.0 兼容方案:AsmClassVisitorFactory
    1. 2.1. 2.1 注册工厂
    2. 2.2. 2.2 Gradle Plugin 注册
  3. 3. 三、ClassVisitor 实现
  4. 4. 四、MethodVisitor:onClick 方法插桩
  5. 5. 五、MethodVisitor:setOnClickListener 调用扫描
    1. 5.1. 5.1 wrapOnClickListener 辅助方法
  6. 6. 六、栈帧(Stack Frame)计算
    1. 6.1. 6.1 COMPUTE_FRAMES 模式
    2. 6.2. 6.2 帧计算性能影响
  7. 7. 七、AGP 7.0 旧 Transform API 实现(参考)
  8. 8. 八、构建性能优化
  9. 9. 九、ProGuard/R8 规则
  10. 10. 十、完整项目结构
  11. 11. 面试常考问题
【全埋点方案系列】AppClick全埋点之ASM处理

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 常量速查

// 与全埋点相关的 Opcodes 常量
object AsmOpcodes {
// === JVM 指令 ===
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 // void 返回
const val ARETURN = Opcodes.ARETURN // 对象返回
const val ACONST_NULL = Opcodes.ACONST_NULL // 压入 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 注册工厂

/**
* AGP 8.0+ 兼容的 ASM Instrumentation
*
* 通过 AsmClassVisitorFactory 实现,替代旧的 Transform API
*
* 项目结构:
* buildSrc/src/main/kotlin/com/example/tracking/plugin/
* ├── TrackingPlugin.kt
* └── TrackingClassVisitorFactory.kt
*/
abstract class TrackingClassVisitorFactory : AsmClassVisitorFactory<InstrumentationParameters.None> {

override fun createClassVisitor(
classContext: ClassContext,
nextClassVisitor: ClassVisitor
): ClassVisitor {
return TrackingClassVisitor(
Opcodes.ASM9, // ASM API 版本
nextClassVisitor,
classContext.currentClassData.className
)
}

override fun isInstrumentable(classData: ClassData): Boolean {
// 过滤规则:
// 1. 排除系统类
// 2. 排除 R 类 / BuildConfig
// 3. 排除测试类
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 注册

/**
* 自定义 Gradle Plugin:注册 ASM 字节码插桩
*/
class TrackingPlugin : Plugin<Project> {

override fun apply(project: Project) {
val androidComponents = project.extensions
.getByType(AndroidComponentsExtension::class.java)

androidComponents.onVariants { variant ->
// 注册 AsmClassVisitorFactory
variant.instrumentation.transformClassesWith(
TrackingClassVisitorFactory::class.java,
InstrumentationScope.ALL // 或 InstrumentationScope.PROJECT
) {
// 配置参数(这里无需参数)
}

// 设置 ASM 帧计算模式
variant.instrumentation.setAsmFramesComputationMode(
FramesComputationMode.COMPUTE_FRAMES
)
}
}
}

三、ClassVisitor 实现

/**
* 类级别的 ASM Visitor
*
* 职责:
* 1. 扫描类实现的接口,判断是否为 OnClickListener
* 2. 对 onClick 方法进行插桩
* 3. 对 setOnClickListener Lambda 也进行插桩
*/
class TrackingClassVisitor(
api: Int,
next: ClassVisitor,
private val className: String
) : ClassVisitor(api, next) {

// 该类是否实现了 OnClickListener 接口
private var isOnClickListener = false

// 该类是否有 onClick 方法(灵活识别)
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

// 检查是否实现了 OnClickListener 接口
isOnClickListener = interfaces?.contains("android/view/View\$OnClickListener") == true

// 也可以检查其他 Listener 接口(用于更广泛的覆盖)
// interfaces?.contains("android/content/DialogInterface\$OnClickListener")
// interfaces?.contains("android/widget/AdapterView\$OnItemClickListener")

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)

// === 场景 1:实现了 OnClickListener 的类的 onClick 方法 ===
if (isOnClickListener && name == "onClick" && descriptor == "(Landroid/view/View;)V") {
return OnClickMethodVisitor(api, mv, className, access, name, descriptor)
}

// === 场景 2:setOnClickListener 调用中传入的 Lambda ===
// 这里需要通过扫描方法体中的指令来判断
// 由于 ClassVisitor 层面无法直接扫描方法体,需要在 MethodVisitor 中处理
// 为此,将 MethodVisitor 设置为 MethodScanner

if (name == "onCreate" || name == "onViewCreated" || name == "onBindViewHolder") {
// 这些方法中常包含 setOnClickListener 调用
return SetOnClickListenerScanner(api, mv, className, access, name, descriptor)
}

return mv
}
}

四、MethodVisitor:onClick 方法插桩

/**
* onClick 方法的插桩 Visitor
*
* 目标:在方法体开头插入 TrackingHelper.trackClick(viewParam)
*
* 原始字节码:
* ALOAD 0 // this
* GETFIELD ...
* ...
* RETURN
*
* 插桩后:
* ALOAD 1 // 第一个参数 (View)
* INVOKESTATIC com/example/tracking/TrackingHelper.trackClick(Landroid/view/View;)V
* // ===== 下面是原始代码 =====
* ALOAD 0
* GETFIELD ...
* ...
* RETURN
*/
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) {

// 解析方法参数,查找 View 参数
private val viewParamIndex = findViewParamIndex(methodDesc)

override fun visitCode() {
// 在方法体的第一条指令之前插入埋点代码
// 等价于:TrackingHelper.trackClick(view);

// Step 1: 加载 View 参数到操作数栈
mv.visitVarInsn(Opcodes.ALOAD, viewParamIndex)

// Step 2: 调用静态方法 trackClick(View)
mv.visitMethodInsn(
Opcodes.INVOKESTATIC,
"com/example/tracking/TrackingHelper",
"trackClick",
"(Landroid/view/View;)V",
false // 不是接口调用
)

// Step 3: 继续执行原始的方法体
super.visitCode()
}

/**
* 从方法描述符中查找 View 参数的索引
*
* 描述符格式:(参数类型)返回类型
* 例如 onClick 的 descriptor = "(Landroid/view/View;)V"
* 对于非静态方法,参数索引 0 = this,索引 1 = 第一个参数
*/
private fun findViewParamIndex(desc: String): Int {
val isStatic = (methodAccess and Opcodes.ACC_STATIC) != 0
var index = if (isStatic) 0 else 1 // 非静态方法从 1 开始(0 是 this)

// 解析参数类型
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 -> { // 基本类型 (I, Z, F, D, J, C, B, S)
if (args[i] == 'D' || args[i] == 'J') index++ // long/double 占两个槽
index++
i++
}
}
}
return 1 // 默认返回第一个参数
}

/**
* 在 visitMaxs 中重新计算栈的最大深度和局部变量表大小
* 由于插入了新的方法调用,栈深度可能增加
*/
override fun visitMaxs(maxStack: Int, maxLocals: Int) {
// 插入 INVOKESTATIC 后,栈深度可能从 N 变为 max(N, 1)
// 因为我们的调用需要栈上有一个 View 引用
super.visitMaxs(maxOf(maxStack, 1), maxLocals)
}
}

五、MethodVisitor:setOnClickListener 调用扫描

/**
* 扫描方法体中的 setOnClickListener 调用
*
* 目标:将 onClick Listener 的参数替换为带埋点的代理 Listener
*
* 原始调用:
* ALOAD 1 // view
* NEW MainActivity$onCreate$1
* DUP
* INVOKESPECIAL MainActivity$onCreate$1.<init>()
* INVOKEVIRTUAL View.setOnClickListener(Landroid/view/View$OnClickListener;)V
*
* 插桩后:
* ALOAD 1 // view
* NEW MainActivity$onCreate$1
* DUP
* INVOKESPECIAL MainActivity$onCreate$1.<init>()
* INVOKESTATIC TrackingHelper.wrapListener(Landroid/view/View$OnClickListener;)Landroid/view/View$OnClickListener;
* INVOKEVIRTUAL View.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
) {
// 检测 setOnClickListener 的调用
if (name == "setOnClickListener" &&
(owner == "android/view/View" || owner == "android/view/ViewGroup") &&
descriptor == "(Landroid/view/View\$OnClickListener;)V"
) {
// 在调用 setOnClickListener 之前插入:
// listener = TrackingHelper.wrapListener(listener)

// 栈顶当前是 listener 对象
// 调用静态方法进行包装
mv.visitMethodInsn(
Opcodes.INVOKESTATIC,
"com/example/tracking/TrackingHelper",
"wrapOnClickListener",
"(Landroid/view/View\$OnClickListener;)Landroid/view/View\$OnClickListener;",
false
)
// 现在栈顶是包装后的 listener
}

// 继续执行原始指令
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface)
}
}

5.1 wrapOnClickListener 辅助方法

/**
* 包装 OnClickListener 的辅助方法(在 TrackingHelper 中)
*
* 注意:此方法被 ASM 插桩调用,需保持签名稳定
*/
object TrackingHelper {

/**
* 包装原始的 OnClickListener
* ASM 插桩时在 setOnClickListener 调用点插入此调用
*
* @param origin 原始 Listener
* @return 包装后的 Listener(内部会先埋点再调用 origin)
*/
@JvmStatic
fun wrapOnClickListener(origin: View.OnClickListener): View.OnClickListener {
// 避免重复包装
if (origin is TrackingOnClickListener) return origin

return TrackingOnClickListener(origin)
}

/**
* 直接埋点(在 onClick 方法开头调用)
*/
@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
}
}

/**
* 带埋点的 OnClickListener 包装器(ASM 使用)
*/
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 模式

/**
* 使用 COMPUTE_FRAMES 简化帧计算
*
* 当插入新指令后,方法的 Stack Map Frame 会变化。
* ASM 提供了三种帧计算模式:
*
* 1. COMPUTE_MAXS (默认):只计算栈深度和局部变量表大小
* 2. COMPUTE_FRAMES:自动计算所有帧,但比 COMPUTE_MAXS 慢 ~50%
* 3. 手动 visitFrame():最快但最容易出错
*/
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

七、AGP 7.0 旧 Transform API 实现(参考)

/**
* AGP 7.x 及以下版本使用的 Transform(已废弃)
* 仅作为历史参考,新项目请使用 AsmClassVisitorFactory
*/
@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 中的目标类名在混淆后发生变化,运行时找不到方法会抛出 NoClassDefFoundErrorNoSuchMethodError。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_FRAMESClassWriter.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?

AsmClassVisitorFactoryInstrumentationScope.ALL 允许处理所有 class 文件(包括 jar/aar 中的),InstrumentationScope.PROJECT 只处理项目自身的源码编译产物。处理第三方库的 class 时,有以下注意事项:(1)对系统库(android.*androidx.*)插桩可能导致崩溃,应在 isInstrumentable() 中排除;(2)第三方库的混淆机制不同,需要确保 TrackingHelper所有 dex 的分包中都能被访问(使用 multidex 的 keep 规则);(3)修改第三方库的 class 可能违反该库的许可协议(如 GPL 的传染性),需要法律审查。

打赏
  • 微信
  • 支付宝

评论