源码分析:Hilt 如何生成组件代码
这一章我们直接走进 Hilt 的编译器内部。说实话,刚接触 Dagger 那会儿,我一直觉得注解处理器是黑魔法——写个 @HiltAndroidApp,项目就能自动拿到 ApplicationComponent。后来出了 Hilt,很多团队直接把它当“Dagger 简化版”用。但问题来了:它到底帮你省了什么?背后的生成代码又长什么样?
我个人习惯是先看生成的 .java 文件,再反推处理器的逻辑。别怕,Hilt 的代码生成没那么玄乎。我带你把核心流程捋一遍。
Hilt 处理器的入口
Hilt 走的还是标准的 javax.annotation.processing 那套。它的核心处理器是 HiltJavaProcessor,继承自 BasicAnnotationProcessor。你可以在项目里搜这个类——嗯,它大概 300 行,不算长。
-Aprocessor 参数,或者在 build 输出里搜 generated/source/kapt(KSP 的话路径不同),你能直接看到 Hilt 生成了什么。
处理器的核心职责就两个:
- 收集入口点 —— 扫描所有被
@HiltAndroidApp、@AndroidEntryPoint、@HiltViewModel标记的类。 - 生成组件代码 —— 为每个入口点创建对应的 Hilt 组件和成员注入器。
注意,它并不会像 Dagger 那样生成一个全局的 DaggerAppComponent。Hilt 生成的组件很多,但每个都很轻量。
源码追踪:从注解到 .java 文件
我们用 @HiltAndroidApp 举个例子。你写:
@HiltAndroidApp
public class MyApp extends Application {}
Hilt 会生成一个 Hilt_MyApp 类,继承你的 MyApp。它里面长这样(简化版):
public class Hilt_MyApp extends MyApp {
@Override
public void onCreate() {
super.onCreate();
// 内部调用了 Hilt 的 ApplicationComponent 初始化
ApplicationComponentHolder.init(this);
}
}
等等,这里有个坑:实际你用的 Application 对象是 Hilt_MyApp,而不是你自己写的类。这就是为什么 AndroidManifest 里必须把 name 写成 .Hilt_MyApp 或 ${applicationClass}_Hilt。我之前有个项目忘了改 manifest,结果跑起来一直空指针,检查了半天才发现是 Hilt 组件没初始化。
核心数据结构:ComponentDescriptor
Hilt 内部用 ComponentDescriptor 来描述每个组件。它包含:
- 组件名称(如
ApplicationComponent) - 对应的 Dagger 组件接口(如
SingletonComponent) - 入口点列表
- 子组件拓扑关系
所有的 ComponentDescriptor 在编译阶段会被构建成一个 组件树。我画了张图,你可以对照着看:
@AndroidEntryPoint 的生成逻辑
我们用 Fragment 来演示——实际上 Activity、Service、BroadcastReceiver 的逻辑类似。你写:
@AndroidEntryPoint
public class MyFragment extends Fragment { ... }
Hilt 做了三件事:
- 生成 Hilt_MyFragment —— 继承你的 Fragment 类,重写关键生命周期方法。
- 创建 ComponentBuilder —— 内部持有一个
FragmentComponentBuilder,用来组装依赖。 - 注入入口点 —— 在
onAttach()中调用FragmentComponent.inject(this)。
生成的代码大概长这样(我摘了关键部分):
public class Hilt_MyFragment extends MyFragment {
private volatile FragmentComponentManager componentManager;
@Override
public void onAttach(Context context) {
super.onAttach(context);
// 初始化 FragmentComponent
componentManager = new FragmentComponentManager(this);
componentManager.inject();
}
}
这里藏的 FragmentComponentManager 是 Hilt 的内部工具类。它负责:
- 从父 Activity 获取 ApplicationComponent
- 创建 FragmentComponent 实例
- 调用生成的 Dagger 组件执行注入
onAttach() 里。如果你在 Fragment 构造函数里访问注入的字段,一定会是 null。我踩过这个坑,后来在 onCreate() 里才敢用。
组件管理的隐藏开销
可能有人觉得,Hilt 既然自动生成代码,那肯定比手写 Dagger 慢。其实不然。它的开销主要体现在两处:
| 阶段 | 真实开销 | 我的建议 |
|---|---|---|
| 编译期 | 生成代码量与 @AndroidEntryPoint 个数线性相关。每个入口点多 2~3 个 .java 文件 | 合理拆分模块,不要在一个 module 里堆上百个 entry point |
| 运行时 | 每个 Fragment/Activity 首次创建时,会执行 Dagger 的注入流程。缓存做好后非常快 | 如果出现启动闪白,优先检查 Application 的初始化,而不是怀疑注入太慢 |
一段关键的生成代码解读
我们打开 Hilt_MyFragment 生成文件,你会发现一个 FragmentComponent 的创建方式:
private FragmentComponent createComponent() {
return DaggerMyFragment_HiltComponents.FragmentComponent.builder()
.applicationComponent(
(ApplicationComponent) componentManager.getApplicationComponent())
.fragmentContext(getContext())
.build();
}
看到没?它实际上委托给了 Dagger 生成的 DaggerMyFragment_HiltComponents。这玩意才是真正的 Dagger 组件。Hilt 只是帮你把这些零散的部分用标准模板串起来。
我曾经做性能分析时,在 createComponent() 前后加过 trace,发现从 ApplicationComponent 获取单例的耗时几乎可以忽略(微秒级)。真正的耗时是首次反射创建 Dagger 组件时,内部要注册所有工厂方法——但只要组件是单例的,后续复用就不会有这开销。
避坑指南:同一 Activity 多个 Fragment 注入
大家注意一个问题:一个 Activity 里如果有 5 个 Fragment 都用 @AndroidEntryPoint,那每个 Fragment 都会创建一个独立的 FragmentComponent。这些组件之间是隔离的,但它们共享同一个 ApplicationComponent。这设计其实很合理——避免了跨 Fragment 的依赖混乱。我之前写过一段代码,想通过 Fragment 的共享 ViewModel 传递数据,结果因为注入的 ViewModel 作用域在同一个 Activity 上,反倒绕了圈子。后来还是老老实实用 @HiltViewModel 加 SavedStateHandle 解决的。