SystemUI Dagger2 精读 · 第 2 章 @Inject:1200 处构造注入,和 1 处例外
一句话结论:
@Inject只做一件事——告诉 Dagger “这个类的这个位置由你来填”。填在构造函数上叫构造注入,填在字段上叫成员注入,两者的时机和约束完全不同。本章解决的问题:为什么我加了
@Inject字段,运行时还是 null?
2.1 机制讲清
构造注入
@SysUISingletonpublicclassInitController{@InjectpublicInitController(){}}Dagger 看到@Inject构造,就为这个类生成InitController_Factory。任何地方需要InitController,Dagger 就调用这个构造,并递归解决参数。
三条硬规则:
- 一个类只能有一个
@Inject构造(否则 Dagger 不知道该用哪个)。 - 无参构造 +
@Inject等价于"我随时可以被造出来"。 - 构造注入的字段可以全 final,这是它最大的价值。
成员(字段)注入
@SysUISingletonpublicclassDependency{@InjectLazy<ActivityStarter>mActivityStarter;@InjectLazy<BroadcastDispatcher>mBroadcastDispatcher;// ...还有 138 个@InjectpublicDependency(){}}成员注入不会在new Dependency()时发生。它必须由组件主动执行:
Dependencydependency=mSysUIComponent.createDependency();dependency.start();createDependency()这个组件方法内部做的才是:new Dependency()→ 遍历所有@Inject字段 → 逐个赋值。字段注入的类,必须在组件接口里声明一个inject(...)方法(或像这样通过组件方法返回实例)。
方法注入
@Inject标注在方法上,由 Dagger 在成员注入阶段调用。SystemUI 里极少用,见到时通常是为了在注入完成后做一次初始化。
2.2 源码定位
| 文件 | 行 | 看什么 |
|---|---|---|
src/com/android/systemui/InitController.java | 34–38 | 最标准的构造注入:@SysUISingleton+@Inject空构造 |
src/com/android/systemui/Dependency.java | 245–374 | 140 个@Inject Lazy<...>字段,全书的"反例中心" |
src/com/android/systemui/Dependency.java | 376–378 | @Inject public Dependency() {}——空构造,依赖全靠字段 |
src/com/android/systemui/Dependency.java | 383–386 | start()里手工把 Lazy 注册进mProviders |
src/com/android/systemui/dagger/SysUIComponent.java | 245–260 | 四个void inject(Xxx)成员注入声明 |
src/com/android/systemui/dagger/ReferenceSysUIComponent.java | 52 | void inject(CustomizationProvider)——厂商加的注入点 |
注意SysUIComponent里这一串:
voidinject(SystemUIAppComponentFactoryBasefactory);// 245voidinject(KeyguardSliceProviderkeyguardSliceProvider);// 250voidinject(ClockOptionsProviderclockOptionsProvider);// 255voidinject(PeopleProviderpeopleProvider);// 260这四个全是Android 框架帮忙 new 出来的类(ContentProvider 及其子类),构造被系统调用,你没法让 Dagger 参与,只能在系统交给你实例之后补一次成员注入。第 9 章会展开这条链路。
2.3 依赖图:两条完全不同的解析路径
【构造注入】 A 需要 B ┌──────┐ new B(dep1, dep2) ┌────────────┐ │ A │ ────────────────────► │ B_Factory │ └──────┘ └─────┬──────┘ │ 递归解析 dep1, dep2 ▼ (图里继续找 Provider) 【成员注入】 框架 new 出 B(构造参数为空) ┌──────────┐ ┌──────────────────────────┐ │ new B() │ ──► 字段全是 null ──►│ component.inject(b) │ └──────────┘ │ → B_MembersInjector │ │ .injectXxx(b, provider)│ └──────────────────────────┘ │ ▼ 字段被赋值(此刻才可用)关键差别:构造注入的依赖在对象"存在的那一刻"就齐了;成员注入的对象存在时是个空壳。
2.4 为什么这么设计
为什么 SystemUI 主流是构造注入
InitController这种类,依赖简单、可 final、可单测——构造注入把"依赖齐全"变成了类型系统的保证,你不可能拿到一个半成品。
为什么Dependency偏偏用字段注入
看它的规模:140 个字段。如果改成构造注入:
publicDependency(Lazy<ActivityStarter>a,Lazy<BroadcastDispatcher>b,/* ... 再写 138 个 ... */){}这段代码没法读、没法维护、每次新增一个依赖都要改 140 行的签名。更要命的是,Dependency是迁移期产物——它的注释自己承认了:
Class to handle ugly dependencies throughout sysui until we determine the long-term dependency injection solution.
翻译:这是个临时方案,作者知道它丑。用字段注入 +Lazy是当时唯一能把 140 个依赖"塞进去而不炸"的写法。
为什么字段注入在 SystemUI 里应该被当成"红旗"
因为成员注入必须由外部驱动。忘记调用inject()就是一堆 null,而且编译期不报错。第 12 章会给出判定:看到@Inject字段,先问"谁负责触发注入",答不出来就是定时炸弹。
2.4.1 一段迁移史:为什么工程里会同时存在两种风格
2017 年之前的 SystemUI 只有一种拿依赖的方式:Dependency.get(Xxx.class)。所有单例塞在一个类里,谁都能静态取。后来引入 Dagger,官方的迁移策略写在docs/dagger.md里:
Dagger 2 has been turned on for SystemUI and much of Dependency.java has been converted to use Dagger. Since a lot of SystemUI depends on Dependency, stubs have been added to Dependency to proxy any gets through to the instances provided by dagger, this will allow migration of SystemUI through a number of CLs.
翻译成一句话:不动调用方,先换供料方。
具体做法就是Dependency变成一个巨大的"字段注入壳":
- 它自己
@Inject140 个Lazy,这些Lazy由 Dagger 提供——供料方换成了 Dagger。 start()把这些Lazy塞进mProviders表。- 老的
Dependency.get(X.class)依然能用——调用方一行没改。
于是工程里就出现了两种风格并存的局面,而且会持续很多年(直到今天还有 96 个调用点)。这不是"设计得不好",而是大规模代码库迁移的唯一现实路径:先换内核,再逐个改调用点。
这段历史对你有直接用处:当你在某个类里看到@Inject字段,不要立刻判定"这是坏代码",先看它是不是 2017—2019 年迁移的产物。判断标准不是"用了字段注入",而是"这个类有没有构造注入的通道":
| 类的形态 | 有构造注入通道吗 | 结论 |
|---|---|---|
普通业务类,自己new或被 Dagger 造 | 有 | 应该改成构造注入,字段注入是债 |
Dependency本身 | 有,但 140 个参数不现实 | 保留,但别再往里加 |
| XML inflate 出来的 View | 没有(LayoutInflater造实例) | 只能用Dependency.get()或 setter |
| 由系统造的 Activity/Service/Provider | 前三者有(见第 9 章),Provider 没有 | 用AppComponentFactory/ 反射 |
这张表就是第 12 章决策树的雏形。
2.4.2 一个容易被忽略的顺序问题
构造注入和字段注入还有一个隐蔽差异:构造注入的依赖在构造完成时一定可用,字段注入的依赖在构造完成时一定不可用。
这会导致一种很难查的 bug:
@SysUISingletonpublicclassFoo{@InjectBarmBar;// 字段注入@InjectpublicFoo(){mBar.doWork();// ← NPE,此时 mBar 还是 null}}它甚至不是"偶尔 NPE"——是必然 NPE,但因为这段代码只在某些机型/某些启动路径上被走到,很容易被误判成偶发问题。
规则:任何"构造里就要用依赖"的类,必须用构造注入。字段注入只适合"构造之后才用"的场景,比如Dependency这种纯容器——它构造时什么都不做,所有工作都在start()里。
2.4.3 Java 与 Kotlin 的写法对照
工程里两种语言混用,@Inject的写法差异是常见卡点:
| 场景 | Java | Kotlin |
|---|---|---|
| 构造注入 | @Inject public Foo(Bar b) | class Foo @Inject constructor(private val b: Bar) |
| 字段注入 | @Inject Bar mBar; | @set:Inject lateinit var bar: Bar或@JvmField @Inject |
| 无参构造 | @Inject public Foo() {} | class Foo @Inject constructor() |
| 接口绑定 | @Binds abstract Foo bind(Impl i) | @Binds abstract fun bind(i: Impl): Foo |
两个 Kotlin 专有坑:
坑一:@Inject必须写在constructor关键字前。
classFoo@Injectconstructor(valb:Bar)// ✓classFoo(valb:Bar)// ✗ 没有注入点坑二:Kotlin 的lateinit字段注入需要@set:前缀。
@set:Injectlateinitvarhelper:Helper// ✓ 注在 setter 上@Injectlateinitvarhelper:Helper// 可能注到字段,行为不一致SystemUIAppComponentFactoryBase.kt:49用的就是@set:Inject,这是 Kotlin 里成员注入的推荐写法。
为什么是 setter?因为 Kotlin 的属性默认编译成 private field + getter/setter,@Inject直接标在属性上时,注解的落点由编译器决定(可能落到 field,也可能落到 getter),而 Dagger 需要明确的 field 或 method。@set:去掉歧义。
2.5 动手练习
练习 2-A:验证构造注入的"自动"(10 分钟)
新建一个类:
@SysUISingletonpublicclassBookProbe{@InjectpublicBookProbe(InitControllerinitController){android.util.Log.d("BookProbe","init="+initController);}}然后在SysUIComponent里加一行BookProbe getBookProbe();,在SystemUIInitializer.init()末尾调一次mSysUIComponent.getBookProbe()。
预期:编译通过、日志打印一次。你没有写任何"注册"代码——构造注入不需要注册,这是它和字段注入最大的体感差别。
练习 2-B:制造一次循环依赖(5 分钟)
让BookProbe依赖InitController,同时让InitController依赖BookProbe。
预期:编译期报[Dagger/DependencyCycle] Found a dependency cycle。这个错误是编译期出现的,这是 Dagger 相对反射 DI 的又一大优势——第 11 章会用Lazy打破它。
2.6 常见坑
| 症状 | 根因 | 处理 |
|---|---|---|
@Inject字段运行时是 null | 没人调用component.inject(instance) | 在组件接口声明void inject(X),并在拿到实例后调用 |
| 报 “cannot be provided without an @Inject constructor” | 类没有@Inject构造,也没有@Provides | 二选一:加@Inject构造,或在 Module 里写@Provides |
| Kotlin 类注入不进去 | 注解落在次构造或没写constructor关键字 | 写成class Foo @Inject constructor(...) |
| 每次注入拿到不同实例 | 类没加作用域 | 需要单例就加@SysUISingleton(第 5 章) |
| 字段注入的 final 字段编译不过 | 字段注入靠反射/生成代码赋值,不能是 final | 用构造注入,或去掉 final |
| 在父类构造里用注入字段,拿到 null | 字段注入发生在整个对象构造完成之后 | 把逻辑移到init()或@Inject方法里 |
同一个类在两处被inject()但字段不同 | 成员注入器是同一份,不会有差异 | 检查是不是两个不同的类加载器/两个组件实例 |
本章小结
@Inject只有三种落点:构造、字段、方法。SystemUI 里 1200 处绝大多数是构造。- 构造注入是"自动"的;字段注入必须由组件显式驱动,忘记驱动就是 null。
Dependency的字段注入不是最佳实践,是一个 140 参数构造无法成立时的妥协——它是理解 SystemUI DI 现状的钥匙。- Kotlin 项目要注意注解位置:
class X @Inject constructor()。
下一章:当对象不能由你
new(framework 类、第三方类、需要构造后立刻注册的类),@Inject就无能为力了,这时必须上@Module+@Provides。
← 第 1 章 编译期 | 全书目录 | 第 3 章 @Module 与 @Provides →