依赖注入简介与Hilt概述
聊 Hilt 之前,得先聊聊依赖注入(DI)到底在解决什么问题。说白了,就是「我要用一个东西,不用自己 new」。你在 Activity 里要弹 Toast,调用 Toast.makeText() 就行——Android 系统已经把对象准备好了。但换成网络库、数据库、Repository,你就得自己管生命周期,对吧?手动 new 没毛病,但项目一大了,你会发现自己成了「人肉对象工厂」。
手动依赖注入 —— 年轻时候的硬编码
我刚入行时写过这样的代码:
class LoginViewModel {
private val api = ApiService()
private val repo = LoginRepository(api)
fun login() {
repo.login(...)
}
}
一看就懂,对吧?但问题是:LoginViewModel 和 ApiService、LoginRepository 高度耦合。单元测试你得先 new 三个对象,mock 起来特别麻烦。更难受的是:一旦 ApiService 构造函数变了,所有 new 过它的地方都得改。我曾经维护过一个 30 多人的项目,光改个构造函数参数就改吐了。
DI 的核心价值
依赖注入本质就是「控制反转」—— 对象不要自己创建依赖,交给外部容器注入。好处很实在:
- 解耦:View 只关心业务,不关心依赖怎么来的
- 可测试:替换成 Mock 对象非常方便
- 生命周期管理:单例、作用域等不用手写
一句话总结:DI 让我们把「对象怎么来」和「对象怎么用」彻底分开。
从 Dagger 到 Hilt —— 为什么需要「Hilt 注入隐藏开销」这个课程?
Android 生态里,Dagger 是最成熟的 DI 方案,但学习曲线陡峭。你想想看,声明 @Module、@Component、@Subcomponent,还要自己创建 Component 并管理生命周期……我当年学 Dagger 2 时,光一个「图依赖」的概念就想了两天。
Google 也意识到了这个问题,所以在 2020 年推出了 Hilt。它在 Dagger 基础上封装了一套 Android 专用的注解和组件,开发者只要加几个注解,剩下的事交给编译期代码生成。
但问题来了:这种「零样板代码」的背后,隐藏了哪些运行时开销? 很多人以为 Hilt 只是编译时代价,运行时跟手动 new 差不多。我最初也是这么想的,直到在一次线上卡顿排查中发现,某个页面初始化时 Hilt 的依赖图解析占了 30ms — 这就是隐藏开销。这门课的目的,就是把这些细节翻出来,一个一个看透。
Hilt 怎么帮你管理对象?—— 一张图看懂结构
先别急着写代码,要理解 Hilt,得先看懂它背后的组件树。我画了个图,把核心节点和关系标出来了:
Hilt 的核心注解 —— 一张表快速过
| 注解 | 作用 | 使用位置 |
|---|---|---|
@HiltAndroidApp |
触发 Hilt 代码生成,创建 SingletonComponent | Application 类 |
@AndroidEntryPoint |
为 Activity/Fragment/View 生成注入入口 | Activity, Fragment, View, Service 等 |
@Inject |
标记构造方法或字段,告诉 Hilt 如何提供实例 | 构造函数、字段 |
@Module |
定义第三方或接口的依赖提供方式 | 类(配合 @Provides 或 @Binds) |
@Provides |
在 Module 中写方法,返回依赖实例 | Module 内的方法 |
@Binds |
接口绑定到实现类(更轻量) | Module 内的抽象方法 |
实战第一步:添加 Hilt 到项目
我个人习惯先加 Gradle 依赖,再写 Application 和 Activity 的注解。给你看个最简配置:
// build.gradle (project)
plugins {
id 'com.google.dagger.hilt.android' version '2.50' apply false
}
// build.gradle (module)
plugins {
id 'kotlin-kapt'
id 'com.google.dagger.hilt.android'
}
dependencies {
implementation 'com.google.dagger:hilt-android:2.50'
kapt 'com.google.dagger:hilt-compiler:2.50'
}
@HiltAndroidApp
class MyApplication : Application()
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var repository: Repository
// ...
}
这就完成了基本的依赖注入框架搭建。注意一点:@HiltAndroidApp 必须写,否则 Hilt 不会生成任何组件。我见过有人只加 @AndroidEntryPoint 没加 Application 的注解,编译报错半天找不到原因。
说实话,Hilt 到底「隐」了什么?
它隐藏了:
- 组件生命周期管理:不用自己创建 DaggerComponent,不用写
component.inject(this)。 - 依赖图构建:编译期帮你算好所有依赖顺序,运行时直接拿。
- 作用域绑定:@Singleton 在 SingletonComponent 里,@ActivityScoped 在 ActivityComponent 里,不用手动管理。
但这不代表没有代价。比如:
- 编译期 codegen 的时间:项目大了,每次加个 @Inject 都可能触发增量编译变慢。
- 运行时组件查找:Hilt 内部通过 Map 或 Provider 查找实例,每次注入都有查找开销。
- 内存占用:组件树本身也是对象,会吃掉一些堆空间。
一个小经验:我在一个大型模块化项目里测过,单个 Activity 初始化时 Hilt 的组件创建耗时约 10~40ms,主要取决于注入的字段数量和依赖深度。这通常不会影响用户体验,但如果你的启动页或冷启动场景超过 100ms,就要开始排查了。
避坑指南 —— 我踩过的第一个坑
我曾经在 @Module 里写了一个 @Provides 方法返回 null,结果 Hilt 编译通过了,但运行时 NPE。为什么?因为 Hilt 只检查类型,不检查值。记住:Hilt 不会阻止你返回 null,但它会直接吞掉异常栈吗?不,它会抛出 NullPointerException 告诉你“cannot return null from non-@Nullable provide method”。嗯,虽然提示了,但定位起来还是比正常崩溃麻烦一点。
另外,如果你用了 @Binds,记得方法必须是 abstract,并且只能有一个参数(接口、实现类)。我刚开始写的时候把返回类型写成了具体类,编译报错 “@Binds methods must have exactly one parameter”。
本章小结
这一章我们先铺了个底:DI 是什么、为什么需要 Hilt,以及 Hilt 的核心结构。你心里应该有个大概框架了。下一节会深入 Hilt 的编译期原理,看看那些注解到底生成了什么代码——这才是隐藏开销的真正源头。不过先别急,扎实的概念是后续分析的基础。
核心记忆点:Hilt = Dagger + Android 组件树。组件树分为 SingletonComponent、ActivityComponent、FragmentComponent 等,每个 @AndroidEntryPoint 会绑定到对应组件。