目录
  1. 1. 一、Javassist 架构与核心 API
    1. 1.1. 1.1 核心类模型
    2. 1.2. 1.2 特殊变量体系
【全埋点方案系列】AppClick全埋点之Javassist处理

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

一、Javassist 架构与核心 API

1.1 核心类模型

ClassPool

└── 管理所有 CtClass 对象

├── CtClass (编译时类)
│ ├── CtMethod (方法)
│ │ ├── CtParameter (参数)
│ │ ├── insertBefore() / insertAfter()
│ │ ├── addCatch()
│ │ └── setBody()
│ ├── CtField (字段)
│ ├── CtConstructor (构造器)
│ └── CtBehavior (方法/构造器的基类)

└── ClassPath
├── ClassClassPath (通过 Class 对象)
├── ByteArrayClassPath (通过字节数组)
└── LoaderClassPath (通过 ClassLoader)

1.2 特殊变量体系

变量 含义 可用范围
$0 this 引用 方法体内
$1, $2, ... 第 1, 2, … 个参数 方法体内
$$` | 所有参数数组 (Object[]) | 方法体内 | | `$_` | 返回值 | insertAfter | | `$e` | 异常对象 | catch 块 | | `$r` | 返回值类型 | insertAfter | | `$w` | 包装器类型转换 | 方法体内 | | `$sig` | 参数类型签名数组 | 方法体内 | | `$type` | 返回值类型 (CtClass) | 方法体内 | | `$class` | 当前类 (CtClass) | 方法体内 | ## 二、Android 编译时注入实现 ### 2.1 Gradle Transform + Javassist
/**
* Javassist 编译时 Transform
*
* 在 AGP 的 Transform 阶段,遍历所有 class 文件,
* 对实现了 OnClickListener 的类进行埋点代码注入。
*
* 注意:AGP 7.0+ 废弃了 Transform,AGP 8.0 完全移除。
* 新项目需要迁移到 AsmClassVisitorFactory + Javassist 的组合方案。
*/
abstract class JavassistTrackingTransform : Transform() {

override fun getName() = "javassistTrackingTransform"

override fun getInputTypes() = setOf(QualifiedContent.DefaultContentType.CLASSES)

override fun getScopes() = setOf(
QualifiedContent.Scope.PROJECT,
QualifiedContent.Scope.SUB_PROJECTS,
QualifiedContent.Scope.EXTERNAL_LIBRARIES // 可选:处理第三方库
)

override fun isIncremental() = false // Javassist 不支持增量 Transform

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

// === Step 1: 创建 ClassPool 并添加 ClassPath ===
setupClassPath(pool, transformInvocation)

// === Step 2: 遍历所有 class 文件 ===
transformInvocation.inputs.forEach { input ->
input.directoryInputs.forEach { dirInput ->
processDirectoryInput(pool, dirInput)
}
input.jarInputs.forEach { jarInput ->
processJarInput(pool, jarInput)
}
}

// === Step 3: 输出日志 ===
val duration = System.currentTimeMillis() - startTime
Log.i("JavassistTransform", "Transform completed in ${duration}ms")
}

private fun setupClassPath(pool: ClassPool, invocation: TransformInvocation) {
// 添加 Android SDK 的 classpath
pool.appendClassPath(
ClassClassPath(android.view.View.OnClickListener::class.java)
)
pool.appendClassPath(
ClassClassPath(android.view.View::class.java)
)
// 添加埋点辅助类的 classpath
pool.appendClassPath(
ClassClassPath(com.example.tracking.TrackingHelper::class.java)
)

// 添加所有 transform input 的路径到 classpath
invocation.inputs.forEach { input ->
input.directoryInputs.forEach { dirInput ->
pool.appendClassPath(dirInput.file.absolutePath)
}
}
}

private fun processDirectoryInput(pool: ClassPool, dirInput: DirectoryInput) {
val destDir = dirInput.file // Transform output directory

dirInput.file.walkTopDown()
.filter { it.isFile && it.extension == "class" }
.forEach { classFile ->
try {
processClass(pool, classFile, destDir)
} catch (e: Exception) {
Log.e("JavassistTransform",
"Failed to process ${classFile.name}: ${e.message}")
}
}
}

private fun processJarInput(pool: ClassPool, jarInput: JarInput) {
// JAR 处理(略)
}

/**
* 处理单个 class 文件
*/
private fun processClass(pool: ClassPool, classFile: File, destDir: File) {
val relativePath = classFile.relativeTo(destDir).path
val className = relativePath
.removeSuffix(".class")
.replace(File.separatorChar, '.')

val ctClass: CtClass
try {
ctClass = pool.getCtClass(className)
} catch (e: NotFoundException) {
// 类在 classpath 中找不到,尝试手动加载
ctClass = pool.makeClass(classFile.inputStream())
}

// === 检查是否需要处理 ===
if (ctClass.isFrozen) return
if (ctClass.isInterface || ctClass.isAnnotation || ctClass.isEnum) return

// 判断是否实现了 OnClickListener
if (!implementsOnClickListener(ctClass)) {
ctClass.detach()
return
}

// === 注入埋点代码 ===
injectTrackingCode(pool, ctClass)

// === 写回修改后的 class ===
val outputFile = File(destDir, relativePath)
outputFile.parentFile.mkdirs()
ctClass.writeFile()

ctClass.detach()
}

/**
* 检查类是否实现了 View.OnClickListener 接口
*/
private fun implementsOnClickListener(ctClass: CtClass): Boolean {
// 检查直接实现的接口
val interfaces = ctClass.interfaces
if (interfaces != null) {
for (iface in interfaces) {
if (iface.name == "android.view.View\$OnClickListener") {
return true
}
}
}
// 检查父类是否实现了该接口
val superClass = ctClass.superclass
if (superClass != null && superClass.name != "java.lang.Object") {
return implementsOnClickListener(superClass)
}
return false
}

/**
* 在 onClick 方法中注入埋点代码
*/
private fun injectTrackingCode(pool: ClassPool, ctClass: CtClass) {
try {
// 查找 onClick(View) 方法
val onClickMethod = ctClass.getDeclaredMethod(
"onClick",
arrayOf(pool.get("android.view.View"))
)

// === 注入埋点调用 ===
// 等价于:TrackingHelper.trackClick($1);
// $1 = 方法的第一个参数 = View
onClickMethod.insertBefore(
"""
{
com.example.tracking.TrackingHelper.trackClick($1);
}
""".trimIndent()
)

// === 添加异常捕获 ===
// 等价于:
// try { ... } catch (Exception e) { TrackingHelper.trackError($1, e); throw e; }
onClickMethod.addCatch(
"""
{
com.example.tracking.TrackingHelper.trackClickError($1, $e);
throw $e;
}
""".trimIndent(),
pool.get("java.lang.Exception")
)

Log.i("JavassistTransform",
"Injected tracking code into ${ctClass.name}.onClick(View)")

} catch (e: NotFoundException) {
// 类没有 onClick 方法,正常情况
} catch (e: CannotCompileException) {
Log.e("JavassistTransform",
"Failed to inject code: ${e.message}")
}
}
}
### 2.2 TrackingHelper 辅助类
/**
* 埋点辅助类
* Javassist 注入的代码实际调用的目标类
*
* 注意:此类必须存在于编译 classpath 中且不能被 R8 移除
*/
object TrackingHelper {

private val executor = Executors.newSingleThreadExecutor()

/**
* 埋点入口方法(编译时注入调用)
*
* @param view 被点击的 View
*/
@JvmStatic
fun trackClick(view: View) {
val context = view.context
val info = buildMap {
put("view_class", view.javaClass.name)
put("view_id", view.id)
put("view_text", getViewText(view))
put("page_name", getPageName(context))
put("timestamp", System.currentTimeMillis())
put("inject_method", "javassist")
}

// 异步上报
executor.execute {
AnalyticsSDK.track("app_click", info)
}
}

/**
* 异常捕获方法(编译时注入调用)
*/
@JvmStatic
fun trackClickError(view: View, error: Throwable) {
executor.execute {
AnalyticsSDK.track("click_error", mapOf(
"view_class" to view.javaClass.name,
"error_type" to error.javaClass.name,
"error_message" to (error.message ?: ""),
"timestamp" to System.currentTimeMillis()
))
}
}

private fun getViewText(view: View): String {
return ((view as? TextView)?.text?.toString() ?: "").take(100)
}

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
}
}
### 2.3 更高级的注入策略
/**
* 高级注入策略:识别不同的 Listener 类型并分别处理
*/
class AdvancedJavassistInjector(private val pool: ClassPool) {

/**
* 注入点击开始/结束的耗时追踪
*/
fun injectTimingTracking(ctClass: CtClass) {
try {
val onClickMethod = ctClass.getDeclaredMethod(
"onClick",
arrayOf(pool.get("android.view.View"))
)

// 在方法开头记录开始时间
onClickMethod.insertBefore(
"""
{
long __trackStartTime = System.currentTimeMillis();
// 将开始时间存入 View 的 tag 中(非侵入式)
$1.setTag(com.example.tracking.R.id.tracking_click_start_time,
Long.valueOf(__trackStartTime));
}
""".trimIndent()
)

// 在方法结尾计算耗时并上报
onClickMethod.insertAfter(
"""
{
Long __startTime = (Long) $1.getTag(
com.example.tracking.R.id.tracking_click_start_time);
if (__startTime != null) {
long __duration = System.currentTimeMillis() - __startTime;
com.example.tracking.TrackingHelper.trackClickDuration($1, __duration);
}
}
""".trimIndent()
)

} catch (e: Exception) {
Log.e("Javassist", "Failed to inject timing: ${e.message}")
}
}

/**
* 处理 Kotlin coroutine 的 onClick
*
* Kotlin 的 suspend onClick 编译后会增加 Continuation 参数
*/
fun injectSuspendOnClickTracking(ctClass: CtClass) {
try {
// 匹配带 Continuation 参数的 onClick
val methods = ctClass.declaredMethods
for (method in methods) {
if (method.name == "onClick" && method.parameterTypes.size == 2) {
val firstParam = method.parameterTypes[0]
if (firstParam.name == "android.view.View") {
// 注:Javassist 对 Kotlin coroutine 的支持有限
// 实际生产环境建议使用 ASM 处理此类情况
}
}
}
} catch (e: Exception) {}
}

/**
* 条件注入:只对特定包名下的类注入
*/
fun shouldInject(className: String, includePackages: List<String>): Boolean {
return includePackages.any { className.startsWith(it) } &&
!className.contains("BuildConfig") &&
!className.contains("\$") // 排除 Kotlin 编译器生成的内部类(可选)
}

/**
* 替换方法体而非插入代码
* 适用于完全重写 onClick 的场景
*/
fun replaceOnClickBody(ctClass: CtClass) {
try {
val onClickMethod = ctClass.getDeclaredMethod(
"onClick",
arrayOf(pool.get("android.view.View"))
)

// 完全替换方法体
onClickMethod.setBody(
"""
{
com.example.tracking.TrackingHelper.trackClick($1);
// 这里无法执行原始逻辑(已丢失)
// 仅适用于特定的代码生成场景
}
""".trimIndent()
)
} catch (e: Exception) {}
}
}
## 三、构建性能分析 ### 3.1 Javassist 的编译时开销
操作                       | 耗时 (每class)
---------------------------|---------------
ClassPool.getCtClass() | ~0.5ms
查找接口实现 | ~0.2ms
insertBefore() (简单插入) | ~1-3ms
writeFile() (生成字节码) | ~2-5ms
Total per class | ~5-10ms

1000 个 class 的项目 | ~5-10s 额外构建时间
5000 个 class 的项目 | ~25-50s 额外构建时间
### 3.2 Javassist vs ASM 性能对比 | 操作 | Javassist | ASM | |------|-----------|-----| | 加载 class | 快(字符串解析) | 快(二进制读取) | | 代码注入 | 慢(需编译源码字符串) | 快(直接操作字节码) | | 生成 class | 慢(需要生成+验证字节码) | 快(直接写入字节码) | | 内存占用 | 高(保留源码 AST) | 低(Stream 处理) | | 学习成本 | 低 | 高 | ## 四、运行时动态注入
/**
* Javassist 运行时动态注入
*
* 注意:Android 运行时使用此方案有诸多限制:
* 1. ART 使用 .dex 格式而非 .class 格式
* 2. 运行时生成 .class 需要 DexClassLoader 动态加载
* 3. 多次生成会导致内存泄漏和类加载器膨胀
*
* 不推荐在生产环境使用此方案,仅用于学习调研。
*/
object RuntimeJavassistInjector {

fun inject(context: Context) {
val pool = ClassPool.getDefault()
pool.appendClassPath(LoaderClassPath(context.classLoader))

try {
// 获取需要修改的类
val ctClass = pool.get("com.example.MainActivity\$onCreate\$1")
val onClickMethod = ctClass.getDeclaredMethod("onClick")

onClickMethod.insertBefore(
"""
{
com.example.tracking.TrackingHelper.trackClick($1);
}
""".trimIndent()
)

// 生成修改后的字节码
val modifiedBytes = ctClass.toBytecode()

// 使用 DexClassLoader 加载修改后的类(复杂且不推荐)
// ...

ctClass.detach()
} catch (e: Exception) {
Log.e("RuntimeJavassist", "Failed to inject", e)
}
}
}
## 五、Javassist vs ASM vs AspectJ 深度对比 | 特性 | Javassist | ASM | AspectJ | |------|-----------|-----|---------| | **API 风格** | 源码字符串 | 字节码指令 (Visitor) | 声明式注解 | | **操作单位** | CtClass / CtMethod | ClassNode / MethodNode | Aspect / Pointcut | | **代码注入方式** | `insertBefore("Track.track($1);")` | `mv.visitMethodInsn(INVOKESTATIC, ...)` | `@Around("pointcut()")` | | **类型检查** | 运行时(字符串编译时) | 无(直接操作字节码) | 编译期(ajc 编译时) | | **Kotlin 支持** | 良好(基于 Java 字节码) | 良好 | 部分(suspend 限制) | | **学习成本** | 低 | 高 | 中 | | **灵活度** | 中 | 极高 | 中(受 AOP 模型约束) | | **Android AGP 8.0** | 需适配新 API | 需适配新 API | 插件待适配 | | **维护建议** | 小型项目快速实现 | 大型项目长期维护 | 中等项目,团队有 AOP 经验 | ## 六、常见错误与调试 ### 6.1 ClassPool 找不到类
// 错误原因:ClassPool 的 classpath 不完整
// 解决:添加所有需要的 classpath
fun buildCompleteClassPath(pool: ClassPool, project: Project) {
// 添加 Android SDK
val androidJar = "${android.sdkDirectory}/platforms/${compileSdkVersion}/android.jar"
pool.appendClassPath(androidJar)

// 添加项目的 class 目录
pool.appendClassPath("${project.buildDir}/intermediates/javac/debug/classes")

// 添加所有依赖
configurations.getByName("implementation").forEach { dep ->
pool.appendClassPath(dep.absolutePath)
}
}
### 6.2 注入代码编译错误
// Javassist 的代码字符串编译失败时的调试方法
fun debugInjection(pool: ClassPool, ctClass: CtClass) {
try {
// 逐字符分析注入代码
val code = """
{
com.example.tracking.TrackingHelper.trackClick(${1}); // 错误:大括号冲突
}
""".trimIndent()

// 使用 CtClass.make 验证代码合法性
val testClass = pool.makeClass("Debug_Injection_${System.currentTimeMillis()}")
testClass.addMethod(CtMethod.make("public void test() $code", testClass))

} catch (e: CannotCompileException) {
// 打印详细的编译错误
Log.e("JavassistDebug", "Compile error: ${e.message}")
Log.e("JavassistDebug", "Reason: ${e.reason}")
}
}
## 七、ProGuard/R8 规则
# 保持 Javassist 注入调用的辅助类
-keep class com.example.tracking.TrackingHelper {
public static void trackClick(android.view.View);
public static void trackClickDuration(android.view.View, long);
public static void trackClickError(android.view.View, java.lang.Throwable);
}

# 保持 View.OnClickListener 匿名内部类的 onClick 不被移除
-keepclassmembers class * implements android.view.View$OnClickListener {
public void onClick(android.view.View);
}

# Javassist 运行时依赖(如果使用运行时动态注入)
-dontwarn javassist.**
-keep class javassist.** { *; }
--- ## 面试常考问题 **Q1:Javassist 的 insertBefore 中 $1 代表什么?** Javassist 的 `$` 变量体系:`$0` = this(当前对象引用),`$1, $2, ...` = 方法的第 1, 2, ... 个参数。在 `onClick(View v)` 中,`$1` 即被点击的 View 对象。其他常用变量:`$$
表示所有参数的 Object[] 数组,$_ 表示返回值(仅在 insertAfter() 中可用,$r 可获取返回值的类型),$e 在 catch 块中表示捕获的异常对象,$w 用于包装类型自动装箱/拆箱(如 $w($1.getId()) 将 int 转为 Integer)。$sig$type 分别在源码级提供参数类型签名数组和返回值类型。

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

Android 的 ART 虚拟机使用 .dex 格式,而非 JVM 的 .class 格式。Javassist 生成的字节码须确保在 D8/R8 转换为 dex 前是合法的 JVM 字节码。主要限制:(1)Android 不支持运行时动态生成并加载 class(除非使用 DexClassLoader 多级加载,会造成内存和类加载器泄漏);(2)Android 对 lambda 和方法引用的处理与 JVM 不同(Android 使用脱糖,JVM 使用 invokedynamic);(3)编译时通过 Transform 在 dx 之前修改 class 文件是正确的做法,因为此时 class 文件还是标准的 JVM 格式,尚未转换为 dex。

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

Transform 阶段在 R8 的 code shrinking 之前执行,所以插入的代码会被 R8 视为「已使用」。但如果 TrackingHelper.trackClick() 是一个只有 void 返回且无可见副作用的方法,R8 可能将其判定为 no-op 并移除整个调用。防范措施:(1)在 ProGuard 规则中添加 -keep 规则维持 TrackingHelper 的所有公共方法;(2)在 TrackingHelper 中添加一个被 System.currentTimeMillis() 等有副作用的调用,阻止 R8 的纯函数优化;(3)在 trackClick 方法上标记 @Keep 注解;(4)使用 assumenosideeffects 规则排除 TrackingHelper 类。

Q4:Javassist 的 insertBefore 和 insertAfter 的执行顺序与原始方法体的关系?

当同时使用 insertBeforeinsertAfteraddCatch 时,生成的字节码执行顺序为:insertBefore 代码 → 原始方法体 → insertAfter 代码。具体来说:(1)insertBefore 在最前面执行,可以修改方法参数或提前返回;(2)原始方法体在中间执行;(3)如果设置了 asFinally = trueinsertAfter 代码类似于 finally 块,无论方法正常返回还是抛出异常都会执行(但如果 catch 块中 throw 了新异常,asFinally 的代码不会执行,除非也设置了 addCatch);(4)addCatch 插入的 catch 块包裹了 insertBefore + 原始方法体 + insertAfter 的整个执行过程。

Q5:Javassist 如何处理泛型和注解?

Javassist 对泛型的支持有限:(1)CtClass.getGenericSignature() 获取泛型签名字符串,但 Javassist 不会解析它;(2)CtMethod.getParameterTypes() 返回的是擦除后的类型,不能直接获取泛型参数;(3)可以通过 method.getAttribute(AnnotationsAttribute.invisibleTag) 获取方法注解,field.getAttribute(AnnotationsAttribute.visibleTag) 获取字段注解。对于复杂泛型场景(如 Kotlin 的 reified 泛型或协变),建议使用 ASM 而非 Javassist。

打赏
  • 微信
  • 支付宝

评论