Hilt 依赖注入核心概念:组件、模块、入口
我刚开始接触 Hilt 的时候,说实话被这三个概念绕得有点晕。组件、模块、入口……听着像是建筑行业的术语。但你真正用起来会发现,它们的分工其实很清晰。今天咱们就把它拆开揉碎了讲清楚。
1. 组件(Component):依赖的“容器”与生命周期管理
组件是 Hilt 里最核心的容器。每个组件都绑定了一个 Android 组件的生命周期。比如 SingletonComponent 跟着 Application 走,ActivityComponent 跟着 Activity 走。组件的作用就是管理它里面所有依赖的创建和销毁。
我个人习惯把这看成是一个“储物间”——每个储物间有固定的使用时间。App 启动时 SingletonComponent 打开,App 销毁时它关闭;Activity 创建时 ActivityComponent 打开,Activity 销毁它关闭。
Hilt 预定义了以下组件层次:
| 组件 | 绑定 Android 组件 | 生命周期 |
|---|---|---|
| SingletonComponent | Application | App 进程存续期间 |
| ActivityRetainedComponent | Activity(配置变更存活) | Activity 创建到销毁(含旋转) |
| ActivityComponent | Activity | Activity 创建到销毁 |
| FragmentComponent | Fragment | Fragment 创建到销毁 |
| ViewComponent | View | View 存在期间 |
| ServiceComponent | Service | Service 启动到停止 |
2. 模块(Module):提供依赖的“工厂”
模块就是告诉 Hilt:“嘿,这个类你该这么创建。”用 @Module 注解一个类,在里面写 @Provides 方法,或者直接用 @Binds 把接口和实现绑定起来。
每个模块还必须用 @InstallIn 指定它装在哪个组件里。比如 @InstallIn(SingletonComponent::class) 表示这个模块提供的依赖只会在 App 级别生效。
这里有个重点:模块本身不存活于组件中,模块只是“说明书”。Hilt 会在对应组件初始化时读取模块里的 @Provides / @Binds 方法,把生成的依赖放进组件容器里。
@Module
@InstallIn(ActivityComponent::class)
object NetworkModule {
@Provides
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.build()
}
}
3. 入口(Entry Point):触发注入的“大门”
入口是 Hilt 和你的代码之间的桥梁。最常见的入口就是 @HiltAndroidApp 和 @AndroidEntryPoint。你只要在 Application 类上写 @HiltAndroidApp,在 Activity/Fragment 上写 @AndroidEntryPoint,Hilt 就会自动为这些类生成一个父类(比如 Hilt_MyActivity),并在生命周期回调里执行注入。
但如果你需要注入到一个不是 Android 组件的普通类(比如一个工具类或第三方库的回调),那就要用到 @EntryPoint 手动创建入口了。我记得有一次需要在 ContentProvider 里拿依赖,因为 ContentProvider 不是标准的 Android 组件,只能通过 @EntryPoint 来解决。
@EntryPoint
@InstallIn(SingletonComponent::class)
interface MyEntryPoint {
fun provideRepository(): MyRepository
}
// 使用时:
val entryPoint = EntryPointAccessors.fromApplication(
context,
MyEntryPoint::class.java
)
val repo = entryPoint.provideRepository()
其实 @AndroidEntryPoint 背后也是这么干的,只是 Hilt 帮你自动生成了步骤。
三者如何协作?
一句话概括:模块提供依赖给组件,入口告诉组件什么时候注入。
我用个实际的运行流程帮你梳理一下:
- App 启动,Application 的
@HiltAndroidApp触发,Hilt 初始化SingletonComponent。 - SingletonComponent 查找所有
@InstallIn(SingletonComponent::class)的模块,执行其中的@Provides方法,把生成的依赖(比如数据库、网络接口)存进组件容器。 - 用户打开一个 Activity,这个 Activity 有
@AndroidEntryPoint。Hilt 创建ActivityComponent,并把它依赖的组件(比如 SingletonComponent)作为父组件。 - ActivityComponent 查找
@InstallIn(ActivityComponent::class)的模块,创建 Activity 级别的依赖。 - 当 Activity 里用
@Inject标注字段时,Hilt 从当前组件(ActivityComponent)开始向上查找依赖,找到后赋值。 - Activity 销毁,ActivityComponent 被释放,它里面的依赖也一并销毁。
@Singleton 标注了一个对象,但模块却装在了 ActivityComponent 里。结果这个对象的生命周期依然是 Activity 级别,@Singleton 在这里没有效果。Hilt 只看 @InstallIn,不看 @Singleton 在那个组件之外是否成立。
下面的图把这些关系直观地展示出来了:
从图上你可以看到,模块和入口都指向组件,而组件又绑定到具体的 Android 组件。Hilt 就是靠这套规则实现“依赖从哪来、何时创建、何时销毁”的精确控制。
4. 一个实战排查的思路
假如你遇到 UI 渲染卡顿,怀疑是依赖注入导致的。排查顺序通常是这样的:
- 先看入口:
@AndroidEntryPoint有没有正确标注?如果 Activity 没有标注,Hilt 不会为该 Activity 创建组件,注入会失败或延迟。 - 再看组件:你注入的依赖是放在
ActivityComponent还是SingletonComponent?如果每次创建 Activity 都要重建一个大对象(比如图片加载库),那就要考虑提升到更上层的组件。 - 最后看模块:
@Provides方法里有没有做耗时的同步操作(比如读取 SharedPreferences)?如果有,它会阻塞主线程,直接导致 UI 掉帧。
System.currentTimeMillis() 记录开始和结束时间,如果超过 16ms,就是需要优化的点。
好了,组件、模块、入口这三个概念,你清楚了吗?后续我们会一步步深入每个环节的优化细节。记住这个关系图,排查 UI 开销的时候,顺着箭头找总能发现问题的根源。