4. Hilt注入方式:构造函数注入、字段注入、方法注入
讲注入方式之前,我想先聊个小事。我刚用Hilt那会儿,看到代码里又是@Inject又是@Bind,头都大了。特别是字段注入和构造函数注入,到底选哪个?后来在一次压测中,我亲眼看到字段注入导致的反射开销卡了UI线程——嗯,那之后我才真正理解了它们的区别。
咱们这一章就专门掰扯清楚这三种注入方式:构造函数注入、字段注入、方法注入。重点是怎么选,以及对渲染性能的影响。
4.1 构造函数注入——最推荐的方式
说白了,就是在类的构造函数上加@Inject注解。Hilt会自动帮我们搞定依赖实例化。
class ImageLoader @Inject constructor(
private val cache: CacheManager,
private val decoder: ImageDecoder
) {
fun load(url: String): Bitmap {
// 加载图片
}
}
优点很明显:
- 编译时完成所有依赖绑定,没有运行时反射——零性能损耗。
- 依赖关系清晰,构造参数一目了然。
- 字段不可变(用
val),线程安全。
我做过的几个渲染性能关键型app(比如长列表图像加载、实时滤镜),全部用构造函数注入。因为它对渲染线程完全透明,没有反射、没有代理,速度就是纯Java/Kotlin构造调用的速度。
4.2 字段注入——方便但小心反射
字段注入就是在类的字段上加@Inject。常见于Activity、Fragment,因为这些对象由系统实例化,没法用构造函数。
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var viewModelFactory: ViewModelFactory
@Inject lateinit var analyticsTracker: AnalyticsTracker
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
这里有个关键点:字段注入是通过反射实现的。Hilt在onCreate之前会走到一个自动生成的Hilt_MainActivity类,里面用反射去查找@Inject字段并赋值。每次创建Activity都要走一遍这个反射过程。
我的建议: Activity/Fragment中只放必要的、与生命周期强相关的依赖。数量控制在3个以内。其他依赖通过构造函数注入传给ViewModel或UseCase。
4.3 方法注入——用得少,但有时是唯一选择
方法注入是在普通方法上加@Inject。Hilt会在对象创建后调用这个方法,传参进去。
class NetworkClient {
private lateinit var interceptor: HttpInterceptor
@Inject
fun attachInterceptor(interceptor: HttpInterceptor) {
this.interceptor = interceptor
// 还可以做一些初始化配置
}
}
为什么会有这个?最常见的是:你想在依赖注入后执行一些初始化逻辑,但又不能放到构造函数里(比如依赖需要循环引用,或者构造参数太多)。
性能上,方法注入和字段注入一样,底层也依赖反射。不过它多了一次方法调用。在渲染性能敏感的路径上,我从未见过有人主动用它。
4.4 三种注入方式对比
| 注入方式 | 实现机制 | 运行时开销 | 适用场景 | 对渲染性能影响 |
|---|---|---|---|---|
| 构造函数注入 | 编译时生成代码,直接new + 传参 | ≈0 | 绝大多数自定义类(ViewModel、UseCase、工具类) | 无影响 |
| 字段注入 | 反射查找@Inject字段并赋值 |
每次创建对象时1-3ms(取决于字段数量) | Activity、Fragment、Service等系统创建的类 | 反射在UI线程执行,可能导致轻微卡顿 |
| 方法注入 | 反射查找@Inject方法并调用 |
每次创建对象时额外方法调用开销 | 需要注入后初始化、循环依赖等边缘场景 | 不推荐在UI关键路径使用 |
4.5 选择策略:从渲染性能出发
你想想看,渲染性能最怕的是什么?是UI线程被阻塞。反射调用虽然单次只有几毫秒,但如果在onCreate(UI线程)里一次反射好几个字段,再加上inflate布局、测量、绘制……帧率就悬了。
我个人的选择原则:
- 能用构造函数注入,绝不用字段注入。 把所有通过依赖注入产生的对象都扔到ViewModel或UseCase的构造里。
- Activity/Fragment里只放生命周期最必要的依赖,比如ViewModelFactory、SavedStateHandle。数量控制在1~2个。
- 永远不要在方法注入里做重量级初始化。如果一定要做,用
@BindInstance或ActivityCreated回调延迟执行。
我记得有个线上问题:一个Feed流页面,每滑出一屏就创建一个新的Fragment(懒加载),每个Fragment字段注入了6个依赖。低端机滑动时,onCreate里反射6次 × 每屏创建2个Fragment = 反射12次。再加上图片解码、布局重新计算,帧率直接掉到20fps。改成构造函数注入后,渲染帧率恢复到55fps以上——就差这么几行代码。