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 处理器的职责链——一张图看懂
3.4 @HiltAndroidApp 是怎么触发的?
这个注解必须放在Application类上。处理器一拿到它,会做三件事:
- 校验: 检查是不是只在一个类上加了,有没有重复,包名是否正确。我曾经遇到过一个案例,同事把
@HiltAndroidApp加到了BaseApplication上,又在子类重复加了一遍——编译直接报错,说“只能有一个”。 - 生成父组件: 生成一个空的
@HiltAndroidApp接口,并让它继承Dagger的@Component。这一步会触发Dagger的处理器去生成DaggerXXXApplication。 - 改写编译单元: 实际上还会生成一个
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 → 最终代码的翻译链条。