注解处理:APT、KAPT、KSP 到底差在哪?

咱们写 Android 项目,但凡用了 Hilt 或者 Dagger,实际上天天都在跟注解处理打交道。你可能只是加了个 @HiltViewModel,然后编译器就自动帮你生成了一堆代码。可你有没有想过——这些代码到底是什么时候生成的?它凭什么能知道你写的 Activity 需要注入什么?

嗯,今天我就带你把这层窗户纸捅破。APT、KAPT、KSP,这三个家伙才是幕后干活的工人。我见过不少团队升级到 KSP 后构建速度提升了一倍,但也见过有人没搞懂原理,直接排查到半夜。

APT:古董级的 Java 注解处理器

APT 全称叫 Annotation Processing Tool,是 JDK 内置的。从 Java 6 开始就有了,它的工作流程其实很直白:

  1. 编译器解析你的 Java 源码文件
  2. 找到所有被注解标记的元素(类、方法、字段)
  3. 启动你写的 AbstractProcessor,给你一轮轮处理机会
  4. 你生成新的 Java 源文件,编译器接着编译那些新文件
一句话总结:APT 只能处理 Java 源码。Kotlin 文件?它不认识。

我早年用 Dagger 2 的时候,全是 Java 项目,APT 跑得溜溜的。后来项目切到 Kotlin,编译时间突然暴涨——当时我就纳闷:明明代码没变多,怎么慢了这么多?

原因很简单:APT 不认识 .kt 文件。Kotlin 编译器必须先把 Kotlin 代码转换成 Java 兼容的“存根”(stubs),然后再交给 APT 去处理。这个过程叫 KAPT。

KAPT:多出来的那一趟弯路

KAPT 的核心思路就是“曲线救国”:

  1. Kotlin 编译器先把 Kotlin 源文件生成 Java 存根(只保留类签名、方法签名,不生成实现)
  2. 这些存根文件看起来就是 Java 源码,APT 能正常处理
  3. APT 生成新的 Java 文件后,再和原始 Kotlin 文件一起编译

听起来挺聪明对吧?但代价是巨大的:

  • 生成存根本身需要时间
  • 存根里包含的是纯 Java 信息,Kotlin 特有的特性(如密封类、内联类)会丢失
  • 每次注解处理都要启动 Java 编译器,整个流程分裂成两个阶段

我曾经在一个中型项目(大约 200 个 Hilt Module)上做过测试:纯 KAPT 的增量编译要 40 秒,改成 KSP 后直接降到 12 秒。差 3 倍多。

避坑提醒:我曾经遇到过一个诡异问题——KAPT 在处理 Kotlin 密封类时生成的代码里枚举常量丢了,排查了两天才发现是存根阶段把 sealed 关键字当成普通 class 处理了。后来换了 KSP 就没事了。

KSP:为 Kotlin 生的符号处理器

KSP(Kotlin Symbol Processing)是 Google 推出的,专门针对 Kotlin 的注解处理方案。它不再是“先翻译再处理”,而是直接啃 Kotlin 的语法树(AST)。

  1. Kotlin 编译器内部维护了符号表(Symbol Table)
  2. KSP 直接查询符号表,拿到注解信息
  3. 处理完直接生成代码,不用中间存根
  4. 整个过程都跑在 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 分钟,每月能省下上千块云成本。你想想看,优化这步到底值不值?

核心原则:能用 KSP 的库,坚决不用 KAPT。只有遇到 KSP 还未支持的库时,才退而求其次。

一张图看懂三者的处理路径

下面的 SVG 清晰展示了从源文件到注解处理完成的三条路径,以及每一步的耗时大头在哪里。

APT / KAPT / KSP 处理流程对比 Java 源文件 (.java) Kotlin 源文件 (.kt) ① KAPT:生成 Java 存根 (耗时瓶颈:完整解析语法树+写文件) ② APT 处理 Java 源文件/存根 (Round 1..N,生成新 Java 文件) ③ KSP 直接处理 Kotlin 符号 (无存根,利用编译器符号表) 生成 Java/Kotlin 源文件 + 最终编译 ⚠️ 关键性能差异 • APT 路径:纯 Java 无需存根,最快 • KAPT 路径:多一步存根生成 + 两次编译,最慢 • KSP 路径:跳过存根,直接处理 Kotlin 符号,速度接近 APT
给你的建议:如果你的项目还在用 KAPT 跑 Hilt,赶紧升级到 Hilt 2.50+(支持 KSP)。升级后增量构建速度能提升 1.5~2 倍,全量构建也能快 30% 左右。迁移成本很低,只要改一行 kapt 为 ksp,再替换对应 annotation processor 的 maven 坐标即可。