Dagger与Hilt概述:到底解决了什么痛点
聊依赖注入,绕不开 Dagger。但说实话,很多Android开发者对 Dagger 的第一印象就是「难」。我刚开始接触时也觉得这玩意儿太重了,配置繁琐,报错信息还看不懂。直到有一天,我在一个大型项目里手动管理各种对象实例,维护一个越来越臃肿的「对象工厂」类,调试一个单例生命周期问题花了整整两天……嗯,从那以后,我彻底成了 Dagger 的拥护者。
不过后来 Google 推出了 Hilt,我觉得这才是真正把 Dagger 的力量带给了普通 Android 开发者。这一节,咱们就从头捋一捋这个演进过程。
先说说 Dagger 的原理:编译时帮你「写代码」
Dagger 的核心思想其实很简单:在编译期自动生成依赖注入的代码。它不依赖反射,不走运行时动态查找,而是通过注解处理器在编译阶段就构建好所有依赖关系图。
你只管声明「我需要什么依赖」,Dagger 就会自动生成「怎么创建并传递这些依赖」的代码。我打个比方:就像你写了个需求清单,然后有个代码生成器帮你把实现细节全写好了。
核心机制速览:
- @Module:声明「我负责提供某些依赖」,相当于一个工厂说明书
- @Provides:标注具体如何创建某个对象
- @Component:连接「需求方」和「提供方」,相当于依赖图的根节点
- @Inject:告诉 Dagger「我需要这个对象」或「我可以用这个构造方法被创建」
// 一个最简单的 Dagger 示例
// 1. 声明提供者
@Module
class NetworkModule {
@Provides
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.build()
}
}
// 2. 声明组件
@Component(modules = [NetworkModule::class])
interface AppComponent {
fun inject(activity: MainActivity)
}
// 3. 使用注入
class MainActivity : AppCompatActivity() {
@Inject lateinit var okHttpClient: OkHttpClient
override fun onCreate(savedInstanceState: Bundle?) {
(application as MyApp).appComponent.inject(this)
// 现在 okHttpClient 已经可用
}
}
编译后 Dagger 会生成 DaggerAppComponent 类,里面包含了创建 OkHttpClient 的完整逻辑。你想想看,如果没有 Dagger,你得手动在 Application 里 new 这个、new 那个,还要小心传递顺序,项目大了简直噩梦。
我个人习惯:在调试 Dagger 编译生成的代码时,直接跳到 build/generated/source/apt/ 目录下看生成的类,远比看编译报错日志直观。这个习惯帮我排查过至少十几次依赖循环问题。
Dagger 的「痛点」在哪?
Dagger 虽然强大,但我在项目中遇到过几个很现实的问题:
- 配置门槛高:每个新组件、新模块都要手动管理 Component 层级,稍有不慎就报错
- 组件生命周期难对齐:Android 本身就有一堆生命周期(Activity、Fragment、Service),Dagger 的 Component 生命周期和 Android 生命周期之间没有标准映射
- 模板代码仍然不少:尤其是要自己初始化 Component、手动调用 inject() 这些步骤
说白了,Dagger 是个通用工具,它没有为 Android 专门做优化。这就导致开发者一边享受 DI 的好处,一边要写大量「胶水代码」来适配 Android 框架。
Hilt 做了什么封装?
Hilt 本质上就是「Dagger 的 Android 版特化封装」。它没有重新发明一套 DI 框架,而是在 Dagger 的基础上做了三件事:
| Hilt 特性 | 解决了什么问题 | 我的实际感受 |
|---|---|---|
| 预定义组件 | 自动将 Component 映射到 Android 生命周期 | 不用再手动创建 Component 和配置继承关系 |
| @HiltAndroidApp / @AndroidEntryPoint | 无需手动初始化 Component 和调用 inject() | Activity 和 Fragment 里注入代码大幅减少 |
| @HiltViewModel | ViewModel 自动获得依赖注入支持 | 再也不用写 ViewModelFactory 了 |
| 编译期验证 | Hilt 会检查组件层次是否正确 | 问题提前暴露,比运行时崩溃友好太多 |
// Hilt 版本的依赖注入
@HiltAndroidApp
class MyApplication : Application() {}
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var okHttpClient: OkHttpClient
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate()
// 不再需要手动调用 inject(),Hilt 自动完成
}
}
@Module
@InstallIn(SingletonComponent::class) // 注意这个注解
object NetworkModule {
@Provides
fun provideOkHttpClient(): OkHttpClient { ... }
}
你注意到差别了吗?@InstallIn(SingletonComponent::class) 就是关键——Hilt 帮你绑定了 Android 的生命周期范围。SingletonComponent 对应 Application 级别,ActivityComponent 对应 Activity 级别,你不用再自己画 Component 树了。
我曾经踩过的坑:最开始用 Hilt 时,我习惯把 @InstallIn 写成 InstallIn(少写了 @),结果编译半天不通过。后来养成习惯:看到 Hilt 相关的注解报错,先检查是不是拼写问题,其次看是不是没加 @符号。
Hilt 的核心优势到底在哪?
这块我直接说结论——经过几个项目实战,我认为 Hilt 最值得称道的三个优势是:
- 零模板初始化:一个
@HiltAndroidApp搞定所有组件初始化,不用像 Dagger 那样在 Application 里手动创建 Component - 生命周期内置:你只需要声明
@InstallIn(SingletonComponent::class)或@InstallIn(ActivityComponent::class),Hilt 自动管理对象的创建和销毁时机,这直接影响了后面我们会讲到的「内存泄漏排查」——很多泄漏就是因为对象在错误的生命周期被持有导致的 - 迁移成本低:Hilt 完全兼容 Dagger 的注解。如果你项目里已经用了 Dagger 的 @Module 和 @Provides,改成 Hilt 只需要加个 @InstallIn 注解,其他代码几乎不用动
一句话总结:Dagger 是「通用 DI 框架」,Hilt 是「Android 专用 DI 框架」。Hilt 保留 Dagger 的编译时安全、无反射等所有优点,同时消灭了 Android 开发中繁琐的配置工作。
所以,这门课为什么先从 Hilt 讲起?
因为后面所有关于「内存泄漏排查」的内容,都建立在对 Hilt 组件层次和生命周期管理的理解上。你现在知道了 Hilt 封装了 SingletonComponent、ActivityComponent 等预定义组件,那就更容易理解为什么一个绑在 ActivityComponent 的对象乱引用 Application 里的单例会导致泄漏——这就是咱们后续要深挖的核心场景。
来,看看这张图,把刚才说的演进关系理清楚:
从图里你可以看到,Dagger 和 Hilt 并非替代关系,而是「基础能力」和「Android 专用封装」的关系。Hilt 在 Dagger 的编译时代码生成基础上,额外提供了 Android 组件生命周期管理功能——这恰恰是后面排查内存泄漏时最重要的理论依据。