目录
  1. 1. 一、Javassist 核心 API
  2. 2. 二、Gradle Transform 中集成
  3. 3. 三、更灵活的注入策略
  4. 4. 四、Javassist vs ASM vs AspectJ
  5. 5. 五、运行时 vs 编译时使用
  6. 6. 面试常考问题
【全埋点方案系列】AppClick全埋点之Javassist处理

Javassist(Java Programming Assistant)是一个在源码层面操作字节码的框架。它使用类似 Java 语法的字符串来描述要插入的代码,而无需直接操作字节码指令(如 ASM),极大降低了字节码操作的门槛。在全埋点场景中,Javassist 可以在编译期或运行时修改 onClick 方法。

一、Javassist 核心 API

  • ClassPool:类池,管理所有被修改的 CtClass 对象。
  • CtClass:编译时类表示,对应一个 Java 类。
  • CtMethod:类中的方法,可通过 insertBefore() / insertAfter() 注入代码。
  • CtField:类中的字段。
// 添加依赖
// implementation 'org.javassist:javassist:3.29.2-GA'

二、Gradle Transform 中集成

class JavassistTrackingTransform : Transform() {
override fun getName() = "javassistTracking"

override fun transform(transformInvocation: TransformInvocation) {
val pool = ClassPool.getDefault()

transformInvocation.inputs.forEach { input ->
input.directoryInputs.forEach { dirInput ->
// 将 class 所在目录加入 classpath
pool.insertClassPath(dirInput.file.absolutePath)

dirInput.file.walkTopDown()
.filter { it.isFile && it.extension == "class" }
.forEach { classFile ->
modifyClass(pool, classFile)
}
}
}
}

private fun modifyClass(pool: ClassPool, classFile: File) {
val ctClass = pool.makeClass(classFile.inputStream())

// 检查是否实现了 OnClickListener 接口
val interfaces = ctClass.interfaces
val isOnClickListener = interfaces?.any {
it.name == "android.view.View\$OnClickListener"
} == true

if (!isOnClickListener) {
ctClass.detach()
return
}

// 修改 onClick 方法
val onClickMethod = ctClass.getDeclaredMethod("onClick")
// 在方法第一行插入埋点代码
onClickMethod.insertBefore(
"""com.example.TrackingHelper.trackClick($1);
"""
)

// 写回 class 文件
classFile.outputStream().use { ctClass.toBytecode(it) }
ctClass.detach()
}
}

三、更灵活的注入策略

// insertBefore:在方法第一行插入
onClickMethod.insertBefore(
"{ com.example.TrackingHelper.trackClickStart(\$1); }"
)

// insertAfter:在方法返回前插入(包括 return/throw)
onClickMethod.insertAfter(
"{ com.example.TrackingHelper.trackClickEnd(\$1); }",
false // asFinally: true 表示类似 try-finally,异常时也执行
)

// 使用 $0, $1, $_ 等特殊变量
// $0 = this
// $1 = 第一个参数(View)
// $_ = 返回值(仅在 insertAfter 中可用)

// 添加 catch 语句,捕获异常并上报
onClickMethod.addCatch(
"{ com.example.TrackingHelper.trackClickError(\$1, \$e); }",
pool.get("java.lang.Exception")
)

四、Javassist vs ASM vs AspectJ

特性 Javassist ASM AspectJ
API 风格 源码级字符串 字节码指令 声明式注解
学习成本
灵活度 极高 受 AOP 模型约束
Android 兼容性 良好 优秀 良好(需插件)
性能 运行时较慢 编译期相当

Javassist 的独特优势:可以用 Java 字符串描述插入逻辑,改一行代码等于改一个字符串,极易维护。缺点是字符串中的代码没有编译时检查,错误在运行时才暴露。

五、运行时 vs 编译时使用

Javassist 支持两种模式:

  • 编译时(Gradle Transform):在构建阶段修改 class,零运行时开销。
  • 运行时(动态加载):在 App 启动时通过 ClassPool 动态修改类,灵活性最高但存在性能损耗和 Android 类加载器限制。全埋点推荐编译时方案。

面试常考问题

Q1:Javassist 的 insertBefore 中 $1 代表什么?

Javassist 的 $ 变量体系:$0 = this,$1, $2, ... = 方法参数依次对应。在 onClick(View v) 中,$1 即被点击的 View 对象。$$ 表示所有参数数组,$_ 表示返回值(仅 insertAfter 可用),$e 在 catch 块中表示异常对象。

Q2:Javassist 在 Android 中的类加载限制?

Android 的 ART 虚拟机使用 .dex 格式,而非 JVM 的 .class 格式。Javassist 生成的字节码需确保在 D8/R8 转换为 dex 前是合法的 JVM 字节码。此外,Android 不支持运行时动态生成并加载 class(除非使用 DexClassLoader),因此运行时 Javassist 方案受限。编译时通过 Transform 在 dx 之前修改 class 文件是正确做法。

Q3:Javassist 修改 class 后如何确保不被 R8 移除?

Transform 阶段在 R8 的 code shrinking 之前执行,所以插入的代码会被 R8 视为”已使用”。但如果 TrackingHelper.trackClick() 只有 void 返回且无副作用,R8 可能将其判定为 no-op 并移除。需要添加 ProGuard keep 规则或在方法上标记 @Keep

打赏
  • 微信
  • 支付宝

评论