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里注入了8个字段。每次进入页面,Hilt会依次反射每一个字段的setter。在低端机型上,这8次反射加起来超过3ms。如果页面频繁重建(比如旋转屏幕),垃圾回收和反射叠加,就很容易引起丢帧。

我的建议: Activity/Fragment中只放必要的、与生命周期强相关的依赖。数量控制在3个以内。其他依赖通过构造函数注入传给ViewModel或UseCase。

4.3 方法注入——用得少,但有时是唯一选择

方法注入是在普通方法上加@Inject。Hilt会在对象创建后调用这个方法,传参进去。

class NetworkClient {

    private lateinit var interceptor: HttpInterceptor

    @Inject
    fun attachInterceptor(interceptor: HttpInterceptor) {
        this.interceptor = interceptor
        // 还可以做一些初始化配置
    }
}

为什么会有这个?最常见的是:你想在依赖注入后执行一些初始化逻辑,但又不能放到构造函数里(比如依赖需要循环引用,或者构造参数太多)。

性能上,方法注入和字段注入一样,底层也依赖反射。不过它多了一次方法调用。在渲染性能敏感的路径上,我从未见过有人主动用它。

💡 什么时候用? 我个人只在测试Mock场景里用过。生产代码中99%的情况,构造函数注入或字段注入就够用了。方法注入容易让代码逻辑分散,调试起来特别麻烦。

4.4 三种注入方式对比

注入方式 实现机制 运行时开销 适用场景 对渲染性能影响
构造函数注入 编译时生成代码,直接new + 传参 ≈0 绝大多数自定义类(ViewModel、UseCase、工具类) 无影响
字段注入 反射查找@Inject字段并赋值 每次创建对象时1-3ms(取决于字段数量) Activity、Fragment、Service等系统创建的类 反射在UI线程执行,可能导致轻微卡顿
方法注入 反射查找@Inject方法并调用 每次创建对象时额外方法调用开销 需要注入后初始化、循环依赖等边缘场景 不推荐在UI关键路径使用

4.5 选择策略:从渲染性能出发

你想想看,渲染性能最怕的是什么?是UI线程被阻塞。反射调用虽然单次只有几毫秒,但如果在onCreate(UI线程)里一次反射好几个字段,再加上inflate布局、测量、绘制……帧率就悬了。

我个人的选择原则:

  1. 能用构造函数注入,绝不用字段注入。 把所有通过依赖注入产生的对象都扔到ViewModel或UseCase的构造里。
  2. Activity/Fragment里只放生命周期最必要的依赖,比如ViewModelFactory、SavedStateHandle。数量控制在1~2个。
  3. 永远不要在方法注入里做重量级初始化。如果一定要做,用@BindInstance或ActivityCreated回调延迟执行。

我记得有个线上问题:一个Feed流页面,每滑出一屏就创建一个新的Fragment(懒加载),每个Fragment字段注入了6个依赖。低端机滑动时,onCreate里反射6次 × 每屏创建2个Fragment = 反射12次。再加上图片解码、布局重新计算,帧率直接掉到20fps。改成构造函数注入后,渲染帧率恢复到55fps以上——就差这么几行代码。

4.6 核心知识图谱

Hilt 三种注入方式与渲染性能影响 构造函数注入 @Inject constructor() 字段注入 @Inject lateinit var 方法注入 @Inject fun setDep() 编译时生成 直接 new + 传参 运行时反射 查找 @Inject 字段 运行时反射 查找并调用方法 零开销 执行时间 ≈ 0ms 推荐 反射消耗 1~3ms / 字段 ⚠️ UI线程卡顿风险 反射+方法调用 额外开销 不推荐用于UI路径 渲染性能关键结论: 构造函数注入对渲染线程零影响;字段注入尽量控制在2个以内;方法注入避免在onCreate/onBind中使用。
核心记住一句话: 能放进构造函数的,不要丢给字段反射;能少注入一个,就少反射一次。渲染性能就是在这些微秒级的取舍中被拉开的。