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 根本不知道你想要哪一个。自己定义几个注解,代码可读性反而更好。

---

一张图理清注解之间的关系

下面这张图把上面提到的核心注解、它们之间的约束关系画了出来。你可以把它当作战术板,排查问题时回来对照。

Hilt 核心注解关系图 @Module 声明依赖来源 @InstallIn 指定安装组件 @Provides / @Binds 提供/绑定实例 SingletonComponent ActivityComponent FragmentComponent … 其他 @Singleton @ActivityScoped @FragmentScoped @Qualifier(解决相同类型冲突) 作用域必须对应组件 非法组合编译报错

其他小注解:@EntryPoint 与 @HiltViewModel

最后简单提两个辅助注解:

  • @EntryPoint:当你需要在非 Hilt 支持的类(比如普通的 ContentProvider)里拿到依赖,可以通过 EntryPoint 手动获取。但我建议尽量避免——一旦用了 EntryPoint,代码的依赖关系就变得难以追踪。
  • @HiltViewModel:配合 Hilt 的 ViewModel 使用,让 ViewModel 也能享受自动注入。用法就是在 ViewModel 类上直接加 @HiltViewModel,并用 @Inject constructor 构造。

总结一下核心思维:

  • 用 @Module 组织依赖来源,用 @InstallIn 指定安装组件
  • 第三方类用 @Provides,接口绑定用 @Binds
  • 作用域注解控制实例个数,切记与组件匹配
  • 类型冲突用 @Qualifier 区分,别偷懒用 @Named