Javassist(Java Programming Assistant)是一个在源码层面操作字节码的框架。它使用类似 Java 语法的字符串来描述要插入的代码,而无需直接操作字节码指令(如 ASM),极大降低了字节码操作的门槛。在全埋点场景中,Javassist 可以在编译期或运行时修改 onClick 方法。
一、Javassist 核心 API
- ClassPool:类池,管理所有被修改的 CtClass 对象。
- CtClass:编译时类表示,对应一个 Java 类。
- CtMethod:类中的方法,可通过
insertBefore()/insertAfter()注入代码。 - CtField:类中的字段。
// 添加依赖 |
二、Gradle Transform 中集成
class JavassistTrackingTransform : Transform() { |
三、更灵活的注入策略
// insertBefore:在方法第一行插入 |
四、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。



