3、Hilt编译时注解处理流程:@HiltAndroidApp、@AndroidEntryPoint等注解如何触发处理器

3.1 处理器从哪冒出来的?

很多同学写Hilt时只在Activity上加个@AndroidEntryPoint,就觉得完事了。但你想过没有——这行注解是怎么变成那些烦人的Hilt_XXX类的?

说白了,靠的是Java编译时的注解处理机制(APT)。我个人习惯把APT比喻成一个“扫地机器人”:你在代码里贴了标签(注解),它编译时就会自动扫过去,然后帮你生成一堆辅助代码。Hilt的处理器叫HiltAnnotationProcessor,但它不是直接处理@AndroidEntryPoint的,背后还有一层Dagger的影子。

核心事实: Hilt的注解处理分两层——上层是自己定义的@HiltAndroidApp、@AndroidEntryPoint,下层是Dagger的@Component、@Module。处理器会先把Hilt注解翻译成Dagger注解,然后让Dagger的处理器去干活。

3.2 谁在监听这些注解?

注册处理器的方式有两种,Hilt用的是最常见的那种:通过META-INF/services/javax.annotation.processing.Processor文件。你在依赖的hilt-compiler包里就能找到这个文件,里面写着dagger.hilt.processor.internal.HiltAnnotationProcessor。

我记得有一次排查一个奇怪的编译错误,翻Gradle的依赖树才发现是某个库的hilt-compiler版本和主项目不一致,导致处理器没被正确触发。嗯,版本对齐真的很重要。

// 处理器注册文件内容示例(位于META-INF/services下)
dagger.hilt.processor.internal.HiltAnnotationProcessor

当javac开始编译时,它会扫描classpath里所有jar包下的这个文件,加载里面写的处理器类,然后传给RoundEnvironment。这就是整个流程的起点。

3.3 处理器的职责链——一张图看懂

Hilt编译时注解处理流程 ① 源码中的注解 @HiltAndroidApp / @AndroidEntryPoint ② Javac APT 发现处理器 META-INF/services → HiltAnnotationProcessor ③ HiltAnnotationProcessor.process() 解析注解 → 生成Dagger等效代码 ④ 翻译为 @Component / @Subcomponent 生成 Hilt_XXX 中间代码 + Dagger处理器触发 ⑤ DaggerAnnotationProcessor 生成真正的 DI 工厂类 ⑥ 生成 .class & dagger内部代码 关键提示 Hilt处理器本身不生成最终 注入代码,它是“翻译官”。 真正干活的是Dagger的APT。 整个流程在编译的 annotation processing round 中完成

3.4 @HiltAndroidApp 是怎么触发的?

这个注解必须放在Application类上。处理器一拿到它,会做三件事:

  1. 校验: 检查是不是只在一个类上加了,有没有重复,包名是否正确。我曾经遇到过一个案例,同事把@HiltAndroidApp加到了BaseApplication上,又在子类重复加了一遍——编译直接报错,说“只能有一个”。
  2. 生成父组件: 生成一个空的@HiltAndroidApp接口,并让它继承Dagger的@Component。这一步会触发Dagger的处理器去生成DaggerXXXApplication。
  3. 改写编译单元: 实际上还会生成一个Hilt_XXXApplication类,你的Application会自动继承它。

个人经验: 如果你发现Hilt生成的DaggerXXXApplication类没有生成,八成是@HiltAndroidApp所在的模块没有应用hilt-android-gradle-plugin。别问我怎么知道的……调了一下午。

3.5 @AndroidEntryPoint 的触发路径

这个注解用于Activity、Fragment、View、Service等。处理器的逻辑和@HiltAndroidApp类似,但更复杂:

注解目标 生成代码 附加逻辑
Activity Hilt_XXXActivity 注入ComponentActivity的onCreate
Fragment Hilt_XXXFragment 延迟注入 + 生命周期绑定
View Hilt_XXXView 构造参数注入

处理器会遍历所有带@AndroidEntryPoint的类,然后生成一个对应的Hilt_前缀的基类。注意,生成的代码里会包含对@AndroidEntryPoint注解的引用——这里有个小陷阱:如果你的代码在编译时被ProGuard/R8混淆了注解类,处理器可能识别不到。我建议把dagger.hilt.*整个keep住。

3.6 多轮处理(Round)是个坑

Javac的APT是分多轮(Round)执行的。第一轮处理器扫描原始源码,生成新类;第二轮再扫描已生成的类,看看有没有新的注解需要处理。Hilt的处理器会在第一轮生成Dagger组件的桩代码,Dagger的处理器可能在第二轮才处理那些桩代码。

为什么这是个坑?因为如果两个处理器的优先级或轮次依赖没协调好,生成的代码就会缺胳膊少腿。我记得Hilt的早期版本(2.31之前)就有个bug:在某些Kapt场景下,处理器报“无法找到符号”,就是因为轮次逻辑没处理好。现在的版本已经修复了,但你如果遇到类似错误,可以试试--info编译看看日志里Rounds的输出。

避坑指南: Hilt强依赖编译时的注解处理顺序。如果你同时用了其他APT处理器(比如Room、Glide),并且它们也修改了同一个类,可能会出现冲突。我曾经因为在一个Fragment上同时加了@AndroidEntryPoint和某个自定义注解处理器,结果生成的Hilt_Fragment里找不到成员变量——后来发现是自定义处理器把类结构改了。最终方案是把自定义处理器的处理放到更靠前的轮次,或者干脆不在同一类上混用。

3.7 三句话总结流程

  • 第一步: javac通过META-INF/services加载HiltAnnotationProcessor,属于标准SPI机制。
  • 第二步: 处理器解析@HiltAndroidApp、@AndroidEntryPoint等注解,生成等效的Dagger注解代码(桩)。
  • 第三步: Dagger的处理器在后续轮次中处理这些桩代码,最终产出DaggerXXX工厂类。整个过程是Hilt → Dagger → 最终代码的翻译链条。