☰
# SystemUI Dagger2 精读 · 第 2 章 @Inject:1200 处构造注入,和 1 处例外
2026/10/22 17:51:25 网站建设 项目流程

SystemUI Dagger2 精读 · 第 2 章 @Inject:1200 处构造注入,和 1 处例外

一句话结论:@Inject只做一件事——告诉 Dagger “这个类的这个位置由你来填”。填在构造函数上叫构造注入,填在字段上叫成员注入,两者的时机和约束完全不同。

本章解决的问题:为什么我加了@Inject字段,运行时还是 null?


2.1 机制讲清

构造注入

@SysUISingletonpublicclassInitController{@InjectpublicInitController(){}}

Dagger 看到@Inject构造,就为这个类生成InitController_Factory。任何地方需要InitController,Dagger 就调用这个构造,并递归解决参数。

三条硬规则:

  1. 一个类只能有一个@Inject构造(否则 Dagger 不知道该用哪个)。
  2. 无参构造 +@Inject等价于"我随时可以被造出来"。
  3. 构造注入的字段可以全 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.java34–38最标准的构造注入:@SysUISingleton+@Inject空构造
src/com/android/systemui/Dependency.java245–374140 个@Inject Lazy<...>字段,全书的"反例中心"
src/com/android/systemui/Dependency.java376–378@Inject public Dependency() {}——空构造,依赖全靠字段
src/com/android/systemui/Dependency.java383–386start()里手工把 Lazy 注册进mProviders
src/com/android/systemui/dagger/SysUIComponent.java245–260四个void inject(Xxx)成员注入声明
src/com/android/systemui/dagger/ReferenceSysUIComponent.java52void 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变成一个巨大的"字段注入壳":

  1. 它自己@Inject140 个Lazy,这些Lazy由 Dagger 提供——供料方换成了 Dagger。
  2. start()把这些Lazy塞进mProviders表。
  3. 老的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的写法差异是常见卡点:

场景JavaKotlin
构造注入@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 →

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询