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

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

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

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


2.1 机制讲清

构造注入

java 复制代码
@SysUISingleton
public class InitController {
    @Inject
    public InitController() { }
}

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

三条硬规则:

  1. 一个类只能有一个 @Inject 构造(否则 Dagger 不知道该用哪个)。
  2. 无参构造 + @Inject 等价于"我随时可以被造出来"。
  3. 构造注入的字段可以全 final,这是它最大的价值。

成员(字段)注入

java 复制代码
@SysUISingleton
public class Dependency {
    @Inject Lazy<ActivityStarter> mActivityStarter;
    @Inject Lazy<BroadcastDispatcher> mBroadcastDispatcher;
    // ...还有 138 个

    @Inject
    public Dependency() { }
}

成员注入不会 在 new Dependency() 时发生。它必须由组件主动执行:

java 复制代码
Dependency dependency = 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 里这一串:

java 复制代码
void inject(SystemUIAppComponentFactoryBase factory);   // 245
void inject(KeyguardSliceProvider keyguardSliceProvider); // 250
void inject(ClockOptionsProvider clockOptionsProvider);   // 255
void inject(PeopleProvider peopleProvider);               // 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 个字段。如果改成构造注入:

java 复制代码
public Dependency(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. 它自己 @Inject 140 个 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:

java 复制代码
@SysUISingleton
public class Foo {
    @Inject Bar mBar;              // 字段注入

    @Inject
    public Foo() {
        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 关键字前。

kotlin 复制代码
class Foo @Inject constructor(val b: Bar)   // ✓
class Foo(val b: Bar)                        // ✗ 没有注入点

坑二:Kotlin 的 lateinit 字段注入需要 @set: 前缀。

kotlin 复制代码
@set:Inject lateinit var helper: Helper      // ✓ 注在 setter 上
@Inject lateinit var helper: 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 分钟)

新建一个类:

java 复制代码
@SysUISingleton
public class BookProbe {
    @Inject
    public BookProbe(InitController initController) {
        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 →

相关推荐
小陈从不会敲代码1 小时前
JavaEE面试复习(多线程-初阶)
java·面试·java-ee
周厉1 小时前
酒店PMS系统设计:房型房价管理与动态价格优先级实现(Java/MySQL)
java·数据库·pms酒店系统
一碗白开水一1 小时前
Day 5 · 弹性 | 微观
java·前端·数据库
迪飞特科技1 小时前
大模型接入业务系统:API、本地部署、RAG、智能体四种路线怎么选
java·数据库·人工智能·知识图谱·deepseek·本地化大模型
vipxieliang1 小时前
ValidX 验证器缓存机制:如何提升验证性能
java·spring boot
未秃头的程序猿2 小时前
Spring Cloud Gateway 做统一加解密,我在请求体上栽了三天
java·后端·架构
Rain的Java大神之路2 小时前
别再上来就堆 Redis!点赞系统从打挂数据库到 18w 写峰值,我把这套方案讲透了
java·后端·面试
SimonKing2 小时前
开源文档解析天花板 MinerU 4.0:PDF / 图片 / Office 等任意文档都支持
java·后端·程序员
咖啡八杯2 小时前
Spring MessageSource 国际化消息解析流程
java·后端·spring·架构·框架·若依