编译期处理的原理:Annotation Processor 与 KSP 概述归纳总结

说实话,理解编译期处理,是掌握 Hilt 诊断的第一步。你不必成为编译器专家,但得知道 Hilt 在背后干了什么。每次你点击 Build,Hilt 就开始了一场「代码侦探」行动。

我个人习惯把编译期处理比作「代码的基建阶段」。你在源码里写的 @Inject、@Module 这些注解,就像设计图纸上的标记。真正盖楼的,是编译期的处理器。

2.1 什么是 Annotation Processor?

Annotation Processor(注解处理器),说白了就是编译器里的一个插件。它会在 Java 代码编译期间扫描你写的注解,然后生成新的 Java 代码。

// 举个简单的例子
// 你写的源码
@Inject
lateinit var apiService: ApiService

// 注解处理器会生成这些
public final class SimpleComponent_MembersInjector {
    public void injectMembers(Object instance) {
        // 实际注入逻辑
    }
}

嗯,这里要注意:生成的代码在编译后就和手写代码一样,被编译成字节码。你根本看不到这些文件,但它们实实在在地参与了最终 APK 的构建。

个人小经验:我在项目中曾经遇到一个问题:生成的代码太多,导致编译时间暴涨。后来我发现,问题出在注解处理器没有做好增量编译。记住:处理器设计不好,编译期就会变「便秘」。

2.2 KSP 是什么?为什么要用它?

KSP(Kotlin Symbol Processing)是 Kotlin 官方的编译期处理方案。它直接操作 Kotlin 的 AST(抽象语法树),不需要先转换成 Java。

为什么会这样?因为传统的 Annotation Processor 是基于 Java 的 javax.annotation.processing API。Kotlin 代码想用?得先通过 kapt 转换成 Java stub,再交给处理器。这相当于「绕了个大圈」。

// KSP 的 API 更简洁
interface SymbolProcessor {
    fun process(resolver: Resolver): List<KSAnnotated>
}

// 你可以直接操作 KSFunctionDeclaration
// 而不是先转成 Java 的 Element

你想想看,KSP 省掉了「转成 Java」这一步,速度当然快。KSP 的处理速度通常是 kapt 的 2 倍左右。这是我亲身测试过的,不是吹牛。

避坑警告:我曾经以为 KSP 只是「升级版」的 kapt,直接替换就行。结果呢?KSP 的 API 完全不同,很多 Annotation Processor 的写法在 KSP 里行不通。记得先检查库是否支持 KSP!

2.3 两者的核心差异(必须记住)

对比维度 Annotation Processor KSP
处理语言 Java 源码 / Kotlin stub Kotlin 直接 AST
API 类型 javax.annotation.processing com.google.devtools.ksp
编译速度 较慢(需 stub 生成) 更快(直接处理)
对 Kotlin 特性支持 有限(suspend 函数等不支持) 原生支持
适用场景 传统 Java 项目 / 旧库 Kotlin 优先 / 新项目

我个人建议:新项目直接用 KSP。旧项目如果搬不动,就保持 kapt。但 Hilt 目前(截至最新版本)仍然依赖 Annotation Processor。为什么呢?因为它需要兼容 Java 生态。不过,好消息是 Hilt 团队已经在准备 KSP 支持了。

核心观点:KSP 没有「替换」Annotation Processor,而是「补充」。两者都是编译期生成的利器,只是处理方式和性能不同。Hilt 的编译期诊断,其实就是在两者之间选择合适的技术路径。

2.4 网络请求诊断中的编译期作用

回到我们的主题——网络请求诊断。Hilt 在编译期做了哪些事?其实就两件事:

  1. 验证依赖是否完备。比如你的 Retrofit 实例要注入 OkHttpClient,编译器检查你的 @Module 有没有提供它。
  2. 生成注入代码。把 @Inject 的依赖关系编译成具体代码,运行时直接调用。

我遇到过这样的情况:一个接口调用报 500,排查了半天,发现是 Retrofit 的依赖注入根本没成功。编译期没有报错?因为 Hilt 允许部分依赖在运行时才解析。这点要注意!

// 编译期验证通过的例子
@Module
object NetworkModule {
    @Provides
    fun provideOkHttpClient(): OkHttpClient { ... }

    @Provides
    fun provideRetrofit(client: OkHttpClient): Retrofit { ... }
}

// 编译期检查通过,因为 OkHttpClient 有提供
// 但如果 provideRetrofit 的参数类型写错了,编译器就会报错

你看,编译期的诊断能力是有限度的。它只能检查「类型对不对」,没办法检查「参数传错了」。这也是为什么我们需要运行时诊断工具。

实践建议:在调试网络请求时,我习惯在 @Module 里加一个编译期断言。比如用 @RequiresOptIn 标记一些重要依赖,这样如果我写错了,编译器直接报红。

本章核心图:编译期处理流程

下面这张 SVG 图把 Annotation Processor 和 KSP 的处理流程画在一起。你可以直观地看到两者的区别:一个绕路走 Java stub,一个直通 Kotlin AST。

编译期处理流程对比 (Annotation Processor vs KSP) Annotation Processor Step 1: 扫描 @Inject / @Module Step 2: 生成 Java stub Step 3: 运行 javax.annotation.processing Step 4: 生成 Java 代码 产出: 编译期诊断信息 KSP (Kotlin Symbol Processing) Step 1: 扫描 @Inject / @Module Step 2: 直接操作 Kotlin AST Step 3: 通过 KSP API 处理 Step 4: 生成 Kotlin/Java 代码 产出: 编译期诊断信息 速度 2x KSP 省掉 Java stub 生成

你看左边那条线:Annotation Processor 必须先走 Step 2 生成 Java stub,然后再走 Step 3 的处理器。右边 KSP 直接从 Step 1 跳到 Step 3,中间省掉了 stub 生成。这就是为什么 KSP 更快。

不过要记住:Hilt 目前主要用的是 Annotation Processor。但你完全可以用 KSP 来写自己的诊断工具,用于检查 @Retrofit 的配置是否正确。这叫「两条腿走路」。

一句话总结:Annotation Processor 是「先转化再处理」,KSP 是「直接处理」。两者都能在编译期做网络请求诊断,但 KSP 更擅长 Kotlin 特性诊断,Annotation Processor 更擅长 Java 兼容性诊断。