Hilt依赖注入内存泄漏排查课程 - 第4节
Module 与 InstallIn:依赖从哪来、装到哪去
聊 Hilt 的核心注解,咱们先从 @Module 说起。
@Module 的作用说白了就是告诉 Hilt:「嘿,我这里有一批依赖的制造方法,你拿去用。」它本身不产生任何实例,真正干活的是里面定义的 @Provides 或 @Binds 方法。
但光有 Module 还不够——你得告诉 Hilt 这个 Module 应该装在哪个「组件」里。这就是 @InstallIn 的职责。我最初用 Dagger 时就常犯一个错:写了 Module 却忘了指定安装目标,导致编译失败。Hilt 强制你写 @InstallIn,其实反而省了排查的麻烦。
我的习惯:一个 Module 只安装到一个组件。如果你发现某个 Module 需要同时安装在多个组件里,十有八九是职责没划分清楚。
@InstallIn 的可选组件
| 组件类型 | 生命周期 | 常见用途 |
|---|---|---|
SingletonComponent |
整个 Application | 全局单例、网络库、数据库 |
ActivityComponent |
Activity 创建到销毁 | 界面相关的依赖、ViewModel 工厂 |
FragmentComponent |
Fragment 创建到销毁 | Fragment 专有依赖 |
ViewModelComponent |
ViewModel 存活期间 | ViewModel 内需要的依赖 |
ServiceComponent |
Service 创建到销毁 | 后台服务的依赖 |
为什么组件选择这么重要?因为它决定了依赖的「作用域」和「生命周期」。把一个大对象装进 ActivityComponent 而不用 @Singleton,每次旋转屏幕都会重新创建,内存泄漏隐患就来了。嗯,这个地方非常容易踩坑。
@Provides 与 @Binds:两种「造」依赖的方式
@Provides:你亲手造
当你需要手动构建一个实例时——比如第三方库的类、或者需要传参构造的对象——就用 @Provides。
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.build()
}
@Provides
@Singleton
fun provideRetrofit(client: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(client)
.build()
}
}
注意上面 provideRetrofit 的参数 client: OkHttpClient 是 Hilt 自动注入进来的。你只管声明参数,Hilt 会从依赖图里找到它。我个人觉得这种「依赖自动装配」的感觉非常爽,省了一堆工厂类。
@Binds:你指挥 Hilt 自己去绑定
@Binds 用于接口绑定到实现类。它必须定义在抽象类或接口里,并且方法只能有一个参数——就是你要绑定的实现。
interface AnalyticsService {
fun trackEvent(event: String)
}
class FirebaseAnalytics @Inject constructor() : AnalyticsService {
override fun trackEvent(event: String) {
// 发送到 Firebase
}
}
@Module
@InstallIn(SingletonComponent::class)
abstract class AnalyticsModule {
@Binds
@Singleton
abstract fun bindAnalyticsService(impl: FirebaseAnalytics): AnalyticsService
}
关键区别:@Provides 是「我来写构造逻辑」,@Binds 是「告诉 Hilt 接口配哪个实现」。@Binds 效率更高,因为它不需要生成额外的提供方法,Hilt 直接复用已有的 @Inject 构造。
我曾经在项目里看到一个同事把所有绑定都写成 @Provides,代码又长又容易出错。其实能用 @Binds 的场景,没必要用 @Provides。
@Singleton 与作用域注解:控制实例个数
@Singleton 是其中最常用的一种作用域注解。它告诉 Hilt:这个实例在对应组件内只创建一次。
注意:@Singleton 必须配合 @InstallIn(SingletonComponent::class) 使用。如果你把 @Singleton 加在 ActivityComponent 的 Module 里,编译就会报错——Hilt 会检查作用域是否匹配。
Hilt 提供的作用域注解跟组件对应关系如下:
| 作用域注解 | 对应的组件 | 实例存活周期 |
|---|---|---|
@Singleton |
SingletonComponent |
整个应用进程 |
@ActivityScoped |
ActivityComponent |
单次 Activity 生命周期 |
@FragmentScoped |
FragmentComponent |
单次 Fragment 生命周期 |
@ViewModelScoped |
ViewModelComponent |
ViewModel 存活期间 |
@ServiceScoped |
ServiceComponent |
Service 存活期间 |
避坑指南:我曾经接手过一个项目,里面大量使用 @Singleton 来装饰本该随页面销毁的对象。结果就是用户反复进出页面,内存里堆积了无数旧实例,最后 OOM。记住:作用域越大,内存占用的时间就越长。能用 @ActivityScoped 就不要用 @Singleton。
@Qualifier:解决类型冲突
当同一个接口或类型需要注入多个不同实例时,可以用 @Qualifier 做标记。Hilt 自带了一个 @Named,但我更推荐自定义 Qualifier,因为编译期检查更严格。
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthOkHttpClient
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class NormalOkHttpClient
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
@AuthOkHttpClient
fun provideAuthOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(AuthInterceptor())
.build()
}
@Provides
@Singleton
@NormalOkHttpClient
fun provideNormalOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.build()
}
}
// 使用时
@Inject
@AuthOkHttpClient
lateinit var authClient: OkHttpClient
你想想看,没有 Qualifier 的话,两个 OkHttpClient 的 @Provides 方法放一起,Hilt 根本不知道你想要哪一个。自己定义几个注解,代码可读性反而更好。
一张图理清注解之间的关系
下面这张图把上面提到的核心注解、它们之间的约束关系画了出来。你可以把它当作战术板,排查问题时回来对照。
其他小注解:@EntryPoint 与 @HiltViewModel
最后简单提两个辅助注解:
- @EntryPoint:当你需要在非 Hilt 支持的类(比如普通的 ContentProvider)里拿到依赖,可以通过 EntryPoint 手动获取。但我建议尽量避免——一旦用了 EntryPoint,代码的依赖关系就变得难以追踪。
- @HiltViewModel:配合 Hilt 的 ViewModel 使用,让 ViewModel 也能享受自动注入。用法就是在 ViewModel 类上直接加
@HiltViewModel,并用@Inject constructor构造。
总结一下核心思维:
- 用
@Module组织依赖来源,用@InstallIn指定安装组件 - 第三方类用
@Provides,接口绑定用@Binds - 作用域注解控制实例个数,切记与组件匹配
- 类型冲突用
@Qualifier区分,别偷懒用@Named