4、@Provides与@Binds的编译期生成逻辑归纳总结
@Provides 和 @Binds 是 Hilt 模块里最常用的两个注解。很多同学觉得它们只是声明依赖的两种方式,换着用就行了。其实在编译期,Hilt 对它们的处理逻辑完全不同。我踩过几次坑之后,才算真正搞明白背后的区别。
4.1 核心区别:谁在「干活」
一句话概括:@Provides 是手动撸代码,@Binds 是告诉 Hilt 去接口实现类里找。
你想想看,@Provides 修饰的方法里,你亲自写了 new 对象、调构造、甚至做一些初始化逻辑。Hilt 编译期会把这个方法直接变成工厂类的核心代码。而 @Binds 只是声明「接口 A 的实现类是 B」,Hilt 会自己去 B 的 @Inject 构造函数里生成实例。
我在项目中重构一个网络模块时,曾经把 @Binds 误写成 @Provides,结果发现每次注入都走了额外的方法调用,编译出来的工厂类多了一层包装。后来切回 @Binds,代码体积和编译速度都有改善。
编译期产出对比(以 OkHttpClient 为例)
| 注解 | 编译后工厂类 | 实例化方式 |
|---|---|---|
| @Provides | NetworkModule_ProvideOkHttpClientFactory | 调用 module.provideOkHttpClient() |
| @Binds | 无独立工厂,复用实现类的 Factory | 直接 new OkHttpClient() + 依赖注入 |
4.2 @Provides 的编译期生成逻辑
我们来看一个典型场景。假设你有一个网络配置类,需要从外部传参:
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(
cache: Cache,
interceptor: Interceptor
): OkHttpClient {
return OkHttpClient.Builder()
.cache(cache)
.addInterceptor(interceptor)
.build()
}
}
Hilt 编译期会做这几件事:
- 生成 Factory 类:命名为
NetworkModule_ProvideOkHttpClientFactory,里面包含一个静态方法provideOkHttpClient(...),参数就是方法里的依赖(Cache, Interceptor)。 - 添加 @Inject 标记:把这个 Factory 注册到 Hilt 的依赖图中,声明它提供
OkHttpClient类型。 - 插入调用链:当其他组件需要
OkHttpClient时,Hilt 会调用这个 Factory 的get()方法,内部再调用你的provideOkHttpClient方法。
我个人的习惯:如果依赖的构造很简单(比如单例配置),我倾向用 @Binds + @Inject 构造函数。但如果需要复杂的 builder 模式、额外校验、或者依赖外部参数,那就老老实实写 @Provides。别为了省代码写出隐患。
4.3 @Binds 的编译期生成逻辑
@Binds 只能修饰抽象方法(abstract fun),而且方法体必须为空。它相当于告诉 Hilt:「这个接口,你用那个实现类来搞定。」
@Module
@InstallIn(SingletonComponent::class)
abstract class NetworkBindModule {
@Binds
@Singleton
abstract fun bindApiService(impl: ApiServiceImpl): ApiService
}
Hilt 编译期对 @Binds 的处理逻辑完全不同:
- 不生成新的 Factory:因为实现类
ApiServiceImpl已经有自己的 @Inject 构造函数,Hilt 会直接复用ApiServiceImpl_Factory。 - 建立类型映射:在依赖图中注册一条从
ApiService到ApiServiceImpl的绑定关系。 - 运行时无缝替换:当别人注入
ApiService时,Hilt 查绑定关系,最终拿到ApiServiceImpl的实例。
我曾经栽过的坑:@Binds 方法的参数类型必须与返回值类型有继承/实现关系,否则编译直接报错。而且参数不能是抽象类,必须是具体实现类。如果你在 @Binds 方法里写了任何非空的方法体,Hilt 编译期会直接抛出 @Binds methods must have an empty body 错误。
4.4 编译期生成的代码结构对比
为了直观展示,我用一个经典的场景来对比。假设我们需要提供 Retrofit 实例:
- @Provides 版本:Hilt 会生成类似下面的工厂类(简化版):
public final class NetworkModule_ProvideRetrofitFactory implements Factory<Retrofit> { @Override public Retrofit get() { return NetworkModule.INSTANCE.provideRetrofit(baseUrl, okHttpClient); } } - @Binds 版本:假设你定义
RetrofitProvider接口,然后写一个实现类:
此时 Hilt 只生成class RetrofitProviderImpl @Inject constructor( private val baseUrl: String, private val okHttpClient: OkHttpClient ) : RetrofitProvider { override fun get(): Retrofit { ... } } @Module abstract class BindModule { @Binds abstract fun bindRetrofitProvider(impl: RetrofitProviderImpl): RetrofitProvider }RetrofitProviderImpl_Factory,而 @Binds 方法本身进依赖图里只是做映射,没有额外 Factory。
你发现没?@Provides 生成的 Factory 里直接调了你的模块方法,而 @Binds 完全绕过了模块方法,靠实现类的 Factory 走通。这就是编译期生成逻辑的核心差异。
一张图总结 @Provides vs @Binds 的编译期流程
4.5 实战选择建议
聊了这么多编译期细节,最终你得知道怎么选。我根据自己的经验整理了一个决策表:
| 场景 | 推荐注解 | 原因 |
|---|---|---|
| 依赖需要复杂构造(Builder、校验、配置) | @Provides | 方法体可以写任意逻辑 |
| 接口只有一个实现类,且构造简单 | @Binds | 编译更轻量,无额外 Factory |
| 需要注入第三方库的对象(如 OkHttpClient) | @Provides | 你无法修改第三方类的 @Inject 构造函数 |
| 项目中使用接口编程,实现类经常切换 | @Binds | 容易替换绑定,只需要改 Module 里的抽象方法参数 |
| 依赖有多个构造参数,且部分来自其他模块 | @Provides | 参数自动由 Hilt 传入,方法体内可灵活处理 |
嗯,这里要注意:如果你的 @Binds 方法的参数类型写了父类(而不是具体实现类),编译虽然能过,但 Hilt 会报 Cannot find a binding for ParentType 之类的错误。因为你没有告诉它具体用哪个子类。我遇到过几次这种问题,后来养成习惯,@Binds 的参数一律写具体实现类。
我曾经踩的另一个坑:在同一个 Module 里同时放 @Provides 和 @Binds 方法,结果 Hilt 说类型冲突。后来查了文档才知道,同一个 Module 不要混合提供相同类型的绑定。如果你必须同时存在,考虑拆分成两个 Module,或者用 @Qualifier 区分。不然编译期会报 DuplicateBindings 错误。
最后总结一下:编译期生成逻辑决定了你代码的灵活性和体积。我个人建议,默认优先用 @Binds + @Inject 构造函数,只有遇到第三方库或复杂初始化时才切到 @Provides。这样你的模块代码更干净,Hilt 编译出来的 Factory 也更少。下次你在 Gradle 构建日志里看到几百个 Factory 类时,就知道怎么精简了。