编译时注解处理原理:APT与KSP归纳总结
说到 Hilt 的编译时诊断,咱们得先搞明白两套处理框架:APT 和 KSP。说白了,Hilt 生成那些 @HiltViewModel 的工厂类、处理 @Inject 的时候,就是靠它们在运行。
我个人习惯先理解原理再上手,不然报错时一头雾水。今天我们就来撸一遍两者的核心区别、取舍逻辑,以及我在实际项目里踩过的坑。
APT —— 老牌选手,基于 Javac
APT(Annotation Processing Tool)从 Java 6 就有了,属于编译期的标准扩展。它的工作模式很简单:Javac 在解析源码生成 AST(抽象语法树)之后,会调起所有注册的 Processor,让你遍历元素、生成新文件。
这里有个关键点:APT 处理的是 Java 源文件的 AST。如果项目里混用了 Kotlin,那么 Kotlin 代码中的注解会被 Java 编译器“看到”吗?答案是可以,但要对 Kotlin 编译生成的 .class 做二次处理,或者靠 Kotlin 编译器插件桥接。嗯,这里就容易出问题。
我曾经在混合项目中遇到
@Module 放在 Kotlin 文件里,APT 死活处理不了,后来发现是因为 APT 默认不扫描 Kotlin 源码。不得不靠 KAPT(Kotlin Annotation Processing Tool)把 Kotlin 注解转成 Java Stub,再交给 APT。这步极其耗时!
KSP —— 专为 Kotlin 设计的新方案
KSP(Kotlin Symbol Processing)是 Google 为 Kotlin 量身定制的注解处理框架。它不依赖 Javac,直接解析 Kotlin 的 PSI(程序结构接口)或者编译后的语法树。好处很明显:
- 不需要生成 Stub,处理速度比 KAPT 快 2~5 倍(实测)
- 天然支持 Kotlin 特性:密封类、suspend 函数、内联类等
- 对多平台(KMP)友好
你看,Hilt 从 2.40 版本开始就支持 KSP,也正是这个原因。我自己在 2022 年把一个中型项目从 KAPT 切换到 KSP,编译时间从 4 分钟降到了不到 2 分钟。
核心差异对比
| 维度 | APT (KAPT) | KSP |
|---|---|---|
| 底层依赖 | Javac + AST | Kotlin 内置编译器 |
| 处理对象 | Java 源码(Kotlin 需转 Stub) | Kotlin 直接符号 |
| 编译速度 | 中等(KAPT 较慢) | 快(减少 30%~60% 时间) |
| Kotlin 特性 | 部分支持(需桥接) | 原生支持 |
| 多平台 | 不支持 | 支持 |
| 产生文件数 | 每个 Processor 生成 .java | 生成 .kt 或 .java |
我建议新项目直接上 KSP,老项目如果已经稳定,可以等大版本迁移时一起换。没必要为了换而换。
诊断请求发生在哪个阶段?
你可能会问:Hilt 的编译时额外请求诊断,究竟在哪一步被触发?答案是 注解处理阶段之后,字节码生成之前。具体流程:
- 解析阶段:APT/KSP 扫描所有注解,生成中间文件(如
@HiltViewModel的工厂类) - 聚合阶段:Hilt 处理器收集所有模块、入口点,检查是否有缺失的绑定或循环依赖
- 诊断阶段:根据 Dagger 的图分析,报告“未提供”、“重复绑定”等错误
- 代码生成:生成最终的 Dagger 组件代码
说白了,你看到的大部分 @Hilt 错误信息,都发生在第 2~3 步。KSP 模式下,这些诊断信息解析更快,因为不需要等待 Stub 生成。
如果你的项目使用
hilt-android-gradle-plugin + KSP,请确保版本匹配:Hilt 2.44+ 配合 KSP 1.8.0+。我曾经因为 KSP 版本太低,导致 @InstallIn 注解完全被忽略,编译通过但运行时崩溃,查了两天才发现是版本兼容问题。
一张图看懂 APT vs KSP 的处理路径
下面这张图我把两者编译期处理的完整路径画了出来,你会更清楚地看到诊断触发的时机。
实战:如何配置 KSP 以获得更快的诊断
切换到 KSP 后,你需要在模块级的 build.gradle.kts 里添加如下配置:
plugins {
id("com.google.devtools.ksp") version "1.9.0-1.0.13"
id("com.google.dagger.hilt.android") version "2.50"
}
dependencies {
ksp("com.google.dagger:hilt-compiler:2.50")
// 注意:如果用 kapt,就写 kapt;用 ksp 就写 ksp
}
嗯,这里有个细节:hilt-android-compiler 在 2.44 之后同时支持 APT 和 KSP,但你需要显式把 kapt 换成 ksp,否则它默认走 APT 路径。
编译时打开 Gradle Console,如果看到
KSP: Annotation processing in Kotlin Symbol Processing mode 日志,说明切换成功。
诊断信息的可读性差异
我注意到一个现象:同样的错误(比如缺少 @Provides),KSP 给出的错误信息比 APT 更直接。原因在于 KSP 能保留 Kotlin 源码的行号和符号名称,而 APT 在 Stub 转换时会丢失部分上下文。
举个例子,缺失绑定错误:
APT 报错(典型):
error: [Dagger/MissingBinding] com.example.MyDependency cannot be provided without an @Provides-annotated method.
public abstract static class SingletonC implements HiltInternal_TestApp_HiltComponents.SingletonC {
^
KSP 报错:
[ksp] error: [Hilt] MyViewModel(MyViewModel.kt:15) requires a binding for MyDependency which is not provided by any @Module.
Check the module 'AppModule.kt:42' for missing @Provides.
看到区别了吗?KSP 直接告诉你哪个 ViewModel 的哪一行,以及哪个 Module 需要检查。这就是原生符号处理的优势。
如果你在团队里做 Hilt 迁移,优先让新人用 KSP。报错提示更友好,调试成本低很多。
最后的取舍
要不要从 APT 切到 KSP?我的建议是:
- 纯 Java 项目:留在 APT 完全没问题,KSP 对 Java 符号处理没有性能优势
- Kotlin 为主的项目:强烈推荐 KSP,编译时间少一半,诊断信息更清晰
- KMP 项目:必须用 KSP,APT/KAPT 不支持多平台
说白了,工具选对了,后面几十章的诊断技巧才发挥得出效果。不然你还在等长长的编译时间,哪有心情调 Dagger 报错?