Hilt 依赖注入核心概念:组件、模块、入口

我刚开始接触 Hilt 的时候,说实话被这三个概念绕得有点晕。组件、模块、入口……听着像是建筑行业的术语。但你真正用起来会发现,它们的分工其实很清晰。今天咱们就把它拆开揉碎了讲清楚。

1. 组件(Component):依赖的“容器”与生命周期管理

组件是 Hilt 里最核心的容器。每个组件都绑定了一个 Android 组件的生命周期。比如 SingletonComponent 跟着 Application 走,ActivityComponent 跟着 Activity 走。组件的作用就是管理它里面所有依赖的创建和销毁。

我个人习惯把这看成是一个“储物间”——每个储物间有固定的使用时间。App 启动时 SingletonComponent 打开,App 销毁时它关闭;Activity 创建时 ActivityComponent 打开,Activity 销毁它关闭。

Hilt 预定义了以下组件层次:

组件 绑定 Android 组件 生命周期
SingletonComponentApplicationApp 进程存续期间
ActivityRetainedComponentActivity(配置变更存活)Activity 创建到销毁(含旋转)
ActivityComponentActivityActivity 创建到销毁
FragmentComponentFragmentFragment 创建到销毁
ViewComponentViewView 存在期间
ServiceComponentServiceService 启动到停止
我踩过的坑:曾经把一个网络请求的 Retrofit 实例放在 ActivityComponent 里,导致每次旋转屏幕都要重新创建。项目里后来改成 SingletonComponent 才解决。你想想看,如果你的数据源是全局唯一的,千万别放错组件。

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()
    }
}
我建议:模块里的 provide 方法尽量用短小的函数,只做一件事。别在里面写复杂逻辑,否则测试的时候你会后悔的。

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 帮你自动生成了步骤。

三者如何协作?

一句话概括:模块提供依赖给组件,入口告诉组件什么时候注入。

我用个实际的运行流程帮你梳理一下:

  1. App 启动,Application 的 @HiltAndroidApp 触发,Hilt 初始化 SingletonComponent。
  2. SingletonComponent 查找所有 @InstallIn(SingletonComponent::class) 的模块,执行其中的 @Provides 方法,把生成的依赖(比如数据库、网络接口)存进组件容器。
  3. 用户打开一个 Activity,这个 Activity 有 @AndroidEntryPoint。Hilt 创建 ActivityComponent,并把它依赖的组件(比如 SingletonComponent)作为父组件。
  4. ActivityComponent 查找 @InstallIn(ActivityComponent::class) 的模块,创建 Activity 级别的依赖。
  5. 当 Activity 里用 @Inject 标注字段时,Hilt 从当前组件(ActivityComponent)开始向上查找依赖,找到后赋值。
  6. Activity 销毁,ActivityComponent 被释放,它里面的依赖也一并销毁。
注意陷阱:我曾经在模块里用 @Singleton 标注了一个对象,但模块却装在了 ActivityComponent 里。结果这个对象的生命周期依然是 Activity 级别,@Singleton 在这里没有效果。Hilt 只看 @InstallIn,不看 @Singleton 在那个组件之外是否成立。

下面的图把这些关系直观地展示出来了:

Hilt 依赖注入核心概念关系图 组件 (Component) SingletonComponent ActivityComponent FragmentComponent …… ServiceComponent / ViewComponent 等 模块 (Module) @Module @InstallIn(...) @Provides / @Binds 提供依赖实际对象 入口 (Entry Point) @AndroidEntryPoint @HiltAndroidApp @EntryPoint (自定义) Android 组件 (Application / Activity / Fragment) 模块提供依赖 → 组件管理生命周期 → 入口触发注入,三者缺一不可

从图上你可以看到,模块和入口都指向组件,而组件又绑定到具体的 Android 组件。Hilt 就是靠这套规则实现“依赖从哪来、何时创建、何时销毁”的精确控制。

4. 一个实战排查的思路

假如你遇到 UI 渲染卡顿,怀疑是依赖注入导致的。排查顺序通常是这样的:

  • 先看入口:@AndroidEntryPoint 有没有正确标注?如果 Activity 没有标注,Hilt 不会为该 Activity 创建组件,注入会失败或延迟。
  • 再看组件:你注入的依赖是放在 ActivityComponent 还是 SingletonComponent?如果每次创建 Activity 都要重建一个大对象(比如图片加载库),那就要考虑提升到更上层的组件。
  • 最后看模块:@Provides 方法里有没有做耗时的同步操作(比如读取 SharedPreferences)?如果有,它会阻塞主线程,直接导致 UI 掉帧。
我自己常用的排查技巧:在 Module 的 provide 方法里加个 Log,打印线程名和耗时。比如用 System.currentTimeMillis() 记录开始和结束时间,如果超过 16ms,就是需要优化的点。

好了,组件、模块、入口这三个概念,你清楚了吗?后续我们会一步步深入每个环节的优化细节。记住这个关系图,排查 UI 开销的时候,顺着箭头找总能发现问题的根源。