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 最值得称道的三个优势是:

  1. 零模板初始化:一个 @HiltAndroidApp 搞定所有组件初始化,不用像 Dagger 那样在 Application 里手动创建 Component
  2. 生命周期内置:你只需要声明 @InstallIn(SingletonComponent::class) 或 @InstallIn(ActivityComponent::class),Hilt 自动管理对象的创建和销毁时机,这直接影响了后面我们会讲到的「内存泄漏排查」——很多泄漏就是因为对象在错误的生命周期被持有导致的
  3. 迁移成本低:Hilt 完全兼容 Dagger 的注解。如果你项目里已经用了 Dagger 的 @Module 和 @Provides,改成 Hilt 只需要加个 @InstallIn 注解,其他代码几乎不用动

一句话总结:Dagger 是「通用 DI 框架」,Hilt 是「Android 专用 DI 框架」。Hilt 保留 Dagger 的编译时安全、无反射等所有优点,同时消灭了 Android 开发中繁琐的配置工作。

所以,这门课为什么先从 Hilt 讲起?

因为后面所有关于「内存泄漏排查」的内容,都建立在对 Hilt 组件层次和生命周期管理的理解上。你现在知道了 Hilt 封装了 SingletonComponent、ActivityComponent 等预定义组件,那就更容易理解为什么一个绑在 ActivityComponent 的对象乱引用 Application 里的单例会导致泄漏——这就是咱们后续要深挖的核心场景。

来,看看这张图,把刚才说的演进关系理清楚:

从 Dagger 到 Hilt 的演进与核心概念关系 Dagger 标准堆栈 @Module + @Provides @Component 声明 手动调用 inject() 手动管理组件树 通用框架,不绑定 Android 生命周期 Hilt 封装层 @HiltAndroidApp @AndroidEntryPoint @InstallIn 绑定生命周期 自动初始化和注入 Android 专用,预定义组件层次 Hilt 预定义组件层级(对应 Android 生命周期) SingletonComponent → ActivityComponent → FragmentComponent → ViewComponent → ServiceComponent Hilt 核心优势(与 Dagger 对比) 零模板初始化 无需手动创建 Component 一个 @HiltAndroidApp 搞定 生命周期内置 @InstallIn 绑定 Android 生命周期 对象作用域自动对齐 低迁移成本 兼容 Dagger 所有注解 加 @InstallIn 即可迁移

从图里你可以看到,Dagger 和 Hilt 并非替代关系,而是「基础能力」和「Android 专用封装」的关系。Hilt 在 Dagger 的编译时代码生成基础上,额外提供了 Android 组件生命周期管理功能——这恰恰是后面排查内存泄漏时最重要的理论依据。