目录
  1. 1. 一、AST 方案的核心思想
  2. 2. 二、基于 KSP(Kotlin Symbol Processing)实现
  3. 3. 三、KSP Processor 实现
  4. 4. 四、编译期 AST Transform(Kotlin Compiler Plugin)
  5. 5. 五、AST vs 字节码方案对比
  6. 6. 面试常考问题
【全埋点方案系列】AppClick全埋点之AST处理

AST(Abstract Syntax Tree,抽象语法树)方案是在源码编译阶段进行埋点注入。与 ASM/Javassist 操作字节码不同,AST 操作的是源代码的语法结构树。它通常结合自定义注解 + 注解处理器(APT/KAPT/KSP),在编译期根据注解生成或修改代码,实现埋点。

一、AST 方案的核心思想

AST 方案不修改已有的 class 文件,而是在编译期通过注解处理器生成新的包装类在现有代码中注入埋点片段

流程:源码 → 解析为 AST → 识别埋点注解 → 插入埋点节点 → 生成目标代码 → 编译。

二、基于 KSP(Kotlin Symbol Processing)实现

KSP 是 Google 推出的 Kotlin 符号处理器,比 KAPT 快 2 倍且原生支持 Kotlin 语法结构:

// 1. 定义埋点注解
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.SOURCE)
annotation class TrackClick(
val eventId: String = "",
val properties: String = ""
)

// 2. 业务代码中使用
@TrackClick(eventId = "btn_submit")
fun onSubmitClick(view: View) {
// 业务逻辑
}

三、KSP Processor 实现

class TrackClickProcessor : SymbolProcessor {
override fun process(resolver: Resolver): List<KSAnnotated> {
val symbols = resolver.getSymbolsWithAnnotation("com.example.TrackClick")

symbols.filterIsInstance<KSFunctionDeclaration>().forEach { func ->
val annotation = func.annotations
.first { it.shortName.asString() == "TrackClick" }
val eventId = annotation.arguments.first { it.name?.asString() == "eventId" }.value

// 生成包装函数:创建 FileSpec,在函数调用前后插入埋点
val fileSpec = FileSpec.builder(
func.packageName.asString(),
"${func.simpleName.asString()}_Tracked"
)
.addFunction(
FunSpec.builder(func.simpleName.asString())
.addModifiers(KModifier.PUBLIC)
.addParameter("view", View::class)
.addStatement("AnalyticsSDK.track(%L, mapOf())", eventId)
.addStatement("${func.simpleName.asString()}(view)")
.build()
)
.build()

// 写入生成目录
fileSpec.writeTo(environment.codeGenerator, Dependencies(false))
}
return emptyList()
}
}

四、编译期 AST Transform(Kotlin Compiler Plugin)

Kotlin 编译器插件可以更底层地在 IR(Intermediate Representation)层面修改代码。例如,在 IR 阶段对所有 setOnClickListener { } 调用自动插入埋点:

// Kotlin Compiler Plugin 的 IR Transformer(简化示例)
class TrackingIrTransformer : IrElementTransformerVoid() {
override fun visitFunctionAccess(expression: IrFunctionAccessExpression): IrExpression {
if (expression.isOnClickListenerLambda()) {
// 在 lambda 体前插入 trackClick 调用
insertTrackingCall(expression)
}
return super.visitFunctionAccess(expression)
}
}

五、AST vs 字节码方案对比

维度 AST(KSP/APT) 字节码(ASM/Javassist)
操作层级 源码 AST .class 字节码
生成目标 新源码文件 修改现有 class
编译时安全 高(编译期检查) 中(字节码合法性检查)
灵活性 受注解模型约束 极高(任意修改)
覆盖度 仅注解标记的方法 所有方法(含第三方库)
Kotlin 支持 KSP 原生支持 依赖 Java 字节码兼容

关键差异:AST 方案更适合”白名单”模式(显式标记哪些要埋点),字节码方案更适合”全量”模式(自动覆盖所有点击)。


面试常考问题

Q1:KSP 与 KAPT 的关系和区别?

KSP 是 Google 专为 Kotlin 设计的符号处理器,直接解析 Kotlin 源码的 AST,不需要通过 Java stub 生成中间产物。KAPT 则先将 Kotlin 编译为 Java stub,再用 Java 注解处理器处理。KSP 比 KAPT 快约 2 倍,不产生 Java stub 文件,且能感知 Kotlin 特有语法(如 data class、sealed class)。

Q2:AST 方案能否实现全局无注解全埋点?

不能直接通过 APT/KSP 实现全局无注解埋点,因为它们只能处理带注解的符号。要实现无注解全局埋点,需要使用 Kotlin Compiler Plugin 在 IR 级别(类似 ASM 在字节码级别)对所有方法进行遍历和注入。Compose 的 Compiler Plugin 也是在这一层工作的。

Q3:为什么大多数全埋点框架选择字节码方案而非 AST?

字节码方案(ASM/Javassist/AspectJ)天然支持全局遍历,无需开发者添加注解。AST 方案(APT/KSP)更适合代码生成。全埋点的核心价值是”零业务侵入”,字节码方案真正做到开发者无感知;而 AST 方案要求开发者显式添加注解,这本身就是一种侵入。

打赏
  • 微信
  • 支付宝

评论