注解处理:APT、KAPT、KSP 到底差在哪?
咱们写 Android 项目,但凡用了 Hilt 或者 Dagger,实际上天天都在跟注解处理打交道。你可能只是加了个 @HiltViewModel,然后编译器就自动帮你生成了一堆代码。可你有没有想过——这些代码到底是什么时候生成的?它凭什么能知道你写的 Activity 需要注入什么?
嗯,今天我就带你把这层窗户纸捅破。APT、KAPT、KSP,这三个家伙才是幕后干活的工人。我见过不少团队升级到 KSP 后构建速度提升了一倍,但也见过有人没搞懂原理,直接排查到半夜。
APT:古董级的 Java 注解处理器
APT 全称叫 Annotation Processing Tool,是 JDK 内置的。从 Java 6 开始就有了,它的工作流程其实很直白:
- 编译器解析你的 Java 源码文件
- 找到所有被注解标记的元素(类、方法、字段)
- 启动你写的
AbstractProcessor,给你一轮轮处理机会 - 你生成新的 Java 源文件,编译器接着编译那些新文件
我早年用 Dagger 2 的时候,全是 Java 项目,APT 跑得溜溜的。后来项目切到 Kotlin,编译时间突然暴涨——当时我就纳闷:明明代码没变多,怎么慢了这么多?
原因很简单:APT 不认识 .kt 文件。Kotlin 编译器必须先把 Kotlin 代码转换成 Java 兼容的“存根”(stubs),然后再交给 APT 去处理。这个过程叫 KAPT。
KAPT:多出来的那一趟弯路
KAPT 的核心思路就是“曲线救国”:
- Kotlin 编译器先把 Kotlin 源文件生成 Java 存根(只保留类签名、方法签名,不生成实现)
- 这些存根文件看起来就是 Java 源码,APT 能正常处理
- APT 生成新的 Java 文件后,再和原始 Kotlin 文件一起编译
听起来挺聪明对吧?但代价是巨大的:
- 生成存根本身需要时间
- 存根里包含的是纯 Java 信息,Kotlin 特有的特性(如密封类、内联类)会丢失
- 每次注解处理都要启动 Java 编译器,整个流程分裂成两个阶段
我曾经在一个中型项目(大约 200 个 Hilt Module)上做过测试:纯 KAPT 的增量编译要 40 秒,改成 KSP 后直接降到 12 秒。差 3 倍多。
sealed 关键字当成普通 class 处理了。后来换了 KSP 就没事了。KSP:为 Kotlin 生的符号处理器
KSP(Kotlin Symbol Processing)是 Google 推出的,专门针对 Kotlin 的注解处理方案。它不再是“先翻译再处理”,而是直接啃 Kotlin 的语法树(AST)。
- Kotlin 编译器内部维护了符号表(Symbol Table)
- KSP 直接查询符号表,拿到注解信息
- 处理完直接生成代码,不用中间存根
- 整个过程都跑在 Kotlin 编译器内部
你看,少了“生成存根”和“Java 编译器启动”这两步,速度能不快吗?而且 KSP 是 Kotlin 原生的,能正确识别内联类、类型别名这些特性。
三选一:到底用哪个?
| 对比维度 | APT | KAPT | KSP |
|---|---|---|---|
| 适用语言 | 仅 Java | Java + Kotlin(需存根) | Kotlin 优先,也支持 Java |
| 编译速度 | 快(但仅限纯 Java) | 慢(存根开销 + 多次编译) | 快(直接处理) |
| Kotlin 特性支持 | 不适用 | 部分丢失(密封类、内联类等) | 完整支持 |
| 库生态 | 所有 Java 注解处理器 | 基本覆盖(Hilt、Room、Glide 等) | 逐渐增多(Hilt 2.50+、Room 2.6+) |
| 推荐场景 | 纯 Java 旧项目 | 过渡期、KSP 未支持的库 | 新项目、Kotlin 为主的项目 |
为什么我们这么在意编译时性能?
说白了,Hilt 注解处理是在编译期帮我们生成依赖注入代码的。如果这个过程拖太久,每次改一行代码都要等几十秒编译,那你一天下来至少有半小时在等构建。
我复盘过几个团队的项目:把 Room 从 KAPT 切到 KSP,构建时间平均下降 35%~60%。对于大型项目(200+ 模块),这个差距就是分钟级的。
而且编译时性能还影响 CI——我们公司的 CI 机器每快 1 分钟,每月能省下上千块云成本。你想想看,优化这步到底值不值?
一张图看懂三者的处理路径
下面的 SVG 清晰展示了从源文件到注解处理完成的三条路径,以及每一步的耗时大头在哪里。
kapt 为 ksp,再替换对应 annotation processor 的 maven 坐标即可。