资讯动态

SystemUI Dagger2 精读 · 第 1 章 编译期:Dagger 在你的 build 里到底做了什么

发布时间:2026/10/22 2:28:09 来源:尧图企业网站定制
SystemUI Dagger2 精读 · 第 1 章 编译期Dagger 在你的 build 里到底做了什么一句话结论Dagger 在编译期就把整张依赖图展开成了普通 Java 代码运行时没有任何注解扫描——这是 SystemUI 敢在开机路径上用它做 DI 的唯一理由。本章解决的问题为什么改了注解没生效为什么报错说cannot be provided生成的那堆DaggerXxx到底是什么1.1 机制讲清Dagger 不是运行时框架。它是注解处理器Annotation Processorjavac编译你的源码时Dagger 的 processor 读取Inject/Module/Component然后再生成新的 Java 源码这些生成的源码和你的代码一起被编译成 class。所以一次 build 之后你的工程里会多出三类东西生成物命名规则作用组件实现Dagger组件名Component接口的具体实现持有所有 Provider负责装配工厂类名_Factory每个有Inject构造的类生成一个负责new它并递归传入依赖成员注入器类名_MembersInjector给字段注入用只在类里有Inject字段时生成这段生成的代码是纯手工可读的水平——你完全可以打开看它怎么 new 对象这正是排查 DI 问题的第一现场。fastInit模式是 Dagger 2.30 的一个重要优化SystemUI 打开了它传统模式每个被注入的类生成一个Xxx_Factory耦合深的时候一次注入会产生很长的 state machine 调用链。fastInit模式生成的实现改用SwitchingProvider把取哪个依赖变成一个 switch 分支生成的类数量和方法数显著下降代价是单次首次获取略慢因为多了一层间接。对 SystemUI 这种方法数吃紧、又要在开机时创建上千对象的场景这个取舍非常划算方法数省下来的收益远大于首次 get 的那点延迟。1.2 源码定位文件行看什么Android.bp204dagger2在static_libs里Android.bp226javacflags: [-Adagger.fastInitenabled]Android.bp229plugins: [dagger2-compiler]——Soong 里的注解处理器src/com/android/systemui/dagger/GlobalRootComponent.java24–30关键注释这个接口故意不加Componentsrc/com/android/systemui/dagger/ReferenceGlobalRootComponent.java26–28真正带Singleton Component(modules{GlobalModule.class})的才是图入口src/com/android/systemui/dagger/ReferenceGlobalRootComponent.java41getSysUIComponent()返回子组件 Builder一行代码定义了父子关系GlobalRootComponent的注释值得抄在笔记本第一页/** * Base root component for Dagger injection. * * This class is not actually annotated as a Dagger component, since it is not used directly as one. * Doing so generates unnecessary code bloat. * * See {link ReferenceGlobalRootComponent} for the one actually used by AOSP. */publicinterfaceGlobalRootComponent{这是一个非常典型的 AOSP 手法接口负责契约实现类负责生成代码。如果给这个基接口也加ComponentDagger 会为它生成一整套DaggerGlobalRootComponent而运行时根本不用它——纯浪费代码体积。SystemUI 的SysUIComponent/WMComponent也是同样的套路SysUIComponent加了Subcomponent但ReferenceSysUIComponent才是真正被 build 的那个见第 6 章。1.3 依赖图从源码到 class你的源码 编译期 运行时 ┌──────────────┐ ┌────────────────────┐ ┌──────────────────────┐ │ Inject │ │ dagger2-compiler │ │ DaggerReference- │ │ Module │ ───► │ (javac plugin) │ ───► │ GlobalRootComponent │ │ Component │ │ │ │ .builder().build() │ └──────────────┘ │ 生成: │ │ │ │ │ DaggerReference… │ │ ▼ │ │ Xxx_Factory │ │ 普通 Java 对象 │ │ Xxx_MembersInjector│ │ 无反射、无注解扫描 │ └────────────────────┘ └──────────────────────┘ ▲ │ 输入 ┌────────┴─────────┐ │ -Adagger.fastInit│ │ enabled │ └──────────────────┘注意箭头方向注解信息在编译期被消费掉了生成的 class 里不再需要保留注解。这就是为什么运行时用Class.forName也找不到Inject信息——第 9 章会看到SystemUI 遇到这种情况时只能退回方法名反射inject这种土办法。1.4 为什么这么设计为什么不选反射式 DIGuice / Spring 风格SystemUI 是system_server之外最关键的常驻进程之一开机时要创建上千个对象任何一次反射Class.getDeclaredFields()都是毫秒级开销且会在verify阶段拖慢启动。Dagger 把这一切搬到编译期后运行时成本只剩下new和字段赋值和手写工厂完全一致。为什么不给GlobalRootComponent加注解因为能少生成一个 graph 就少生成一个。SystemUI 有 7 个Component、48 个Subcomponent如果每个基接口都参与生成方法数会直接顶到 dex 上限。AOSP 的做法是只让最终被build()的那个类参与代码生成。fastInit的代价是什么开启后好处生成代码体积小、方法数少、verify快。代价SwitchingProvider.get()里有一次 switch 判断首次获取比传统DoubleCheck多一层跳转调试时栈帧不如传统模式直观。取舍原则方法数紧张的平台SystemUI 正是用 fastInit对启动延迟极端敏感、方法数宽裕的单体 App 可以不开。1.4.1 生成的代码长什么样建立正确的心智模型你不需要背生成代码但必须有一个准确的心智模型否则读报错时会一直猜。传统模式下DaggerXxxComponent大致是这样一个类publicfinalclassDaggerReferenceGlobalRootComponentimplementsReferenceGlobalRootComponent{// 每个绑定一个 Provider 字段privateProviderContextcontextProvider;privateProviderDisplayMetricsdisplayMetricsProvider;...privatevoidinitialize(finalBuilderbuilder){// 用工厂把依赖串起来this.contextProviderInstanceFactory.create(builder.context);this.displayMetricsProviderGlobalModule_ProvideDisplayMetricsFactory.create(contextProvider);// ← 注意入参就是上游 Provider}OverridepublicWMComponent.BuildergetWMComponentBuilder(){returnnewWMComponentBuilder();}}这个结构说明三件事组件就是一堆 Provider 字段的集合。所谓图在代码里就是ProviderA持有ProviderB的引用。initialize()的顺序 依赖顺序。Dagger 在编译期就算好了拓扑序运行时只是照着赋值。这就是为什么循环依赖在编译期就能被抓到——它排不出拓扑序。无作用域的绑定用InstanceFactory有作用域的用DoubleCheck。前者每次get()都调工厂后者缓存第一次结果。fastInit模式下上面那一堆字段会被压缩不再为每个绑定生成独立工厂类而是生成少数几个SwitchingProvider内部按 id 做 switchclassSwitchingProviderTimplementsProviderT{OverridepublicTget(){switch(id){case0:return(T)newDependency(...);case1:return(T)GlobalModule.provideDisplayMetrics(...);...}}}好处是类数量和方法数断崖式下降坏处是调试时栈里全是同一个SwitchingProvider.get()看不出是哪条绑定——这时候只能靠 id 反查生成源码。这个心智模型的实际用途当你怀疑某对象被创建了两次不要猜去生成代码里搜它的 id 或字段名。如果同一个类型出现两个Provider字段说明有两条绑定通常是两个 Module 各提供了一份。1.4.2 报错前缀速查贴在显示器边上前缀含义第一反应[Dagger/MissingBinding]图里没有人能提供这个类型加Inject构造或写Provides[Dagger/DuplicateBindings]同一类型同限定符有两条绑定删一条或加限定符区分[Dagger/DependencyCycle]存在环排不出拓扑序抽公共依赖或谨慎用Lazy断开[Dagger/IncompatibleScopes]作用域与组件不匹配XxxScope要标在对应组件上[Dagger/Nullable]注入了Nullable但提供方不是两边都标或改非空[Dagger/MissingBinding] ... is injected at ...后面那串injected at才是真正有价值的从下往上读找到第一个你自己的类最后一条尤其重要Dagger 的报错是一条链最容易犯的错误是从上往下读被第一行的抽象类型ProviderFoo吓住。正确读法是从injected at往上找第一处你自己的类就是问题源头。1.4.3 构建系统差异Soong 与 Gradle 的注解处理不是一回事你在 App 项目里习惯了这样的配置kapt com.google.dagger:dagger-compiler:$version // Kotlin annotationProcessor com.google.dagger:dagger-compiler:$version // JavaSystemUI 用的是 SoongAndroid.bp对应关系是GradleSoong作用implementation dagger2static_libs: [dagger2]运行时库annotationProcessor dagger-compilerplugins: [dagger2-compiler]注解处理器kapt { arguments ... }javacflags: [-Adagger.fastInitenabled]传给 processor 的参数两处差异要注意Soong 的plugins只作用于javac。Kotlin 代码的注解处理走的是 kotlinc 的 kapt 链路配置位置不同本工程里 Kotlin 侧依赖kotlincflags: [-Xjvm-defaultenable]等配合。所以Java 能编译过、Kotlin 报找不到生成类这类问题根因通常在构建配置而不是注解写错。-Adagger.fastInitenabled是全局开关。它影响整个模块的生成策略不能只对某个类生效。排查思路遇到生成类不存在 / 处理器没跑先确认三件事——运行时库在不在、processor 在不在、编译参数有没有生效。三者缺一症状都是找不到符号。1.5 动手练习练习 1-A看到生成物10 分钟在完整 AOSP 环境下编译 SystemUI 后找这两个文件findout-nameDaggerReferenceGlobalRootComponent.java2/dev/nullfindout-nameDependency_Factory.java2/dev/null打开DaggerReferenceGlobalRootComponent.java找到initialize()方法数一数它里面创建了多少个Provider。你会直观看到Dagger 把哪个对象依赖哪个对象翻译成了构造函数调用。练习 1-B读一次真实报错5 分钟故意在InitController的构造里加一个没人提供的参数InjectpublicInitController(SomeRandomClassx){}编译。观察报错格式error: [Dagger/MissingBinding] com.android.systemui.SomeRandomClass cannot be provided without an Provides-annotated method.记住这个前缀[Dagger/MissingBinding]——它比后面的长文案有用得多直接告诉你是图里缺一个绑定不是注解写错了。预期结果你能在看到任何[Dagger/xxx]时先分类是缺绑定MissingBinding、重复绑定DuplicateBindings、作用域冲突IncompatibleScopes还是循环依赖DependencyCycle。1.6 常见坑症状根因处理改了注解运行没变化增量编译没重新跑 processor全量 rebuild或清out/里对应的Dagger*Kotlin 类加了Inject没生效Kotlin 不走javac插件需 kapt/ksp本工程Android.bp里 Java 与 Kotlin 是两套编译路径报错说DaggerXxx找不到plugins没配或 processor 版本与dagger2运行库不匹配检查Android.bp的plugins: [dagger2-compiler]与static_libs: [dagger2]成对出现生成代码巨大、编译变慢没开fastInit且图很深打开-Adagger.fastInitenabledComponent接口里写了默认方法报奇怪的错接口默认方法在生成实现里被继承但 Dagger 只处理抽象方法默认方法只用于纯逻辑不要在里面Inject字段本章小结编译期生成、运行时零反射这是 Dagger 与所有反射 DI 的分水岭。fastInit是方法数换首次延迟的交易SystemUI 选了方法数。AOSP 用接口不加注解、实现类才生成的方式控制代码体积GlobalRootComponent是活教材。认准[Dagger/xxx]错误前缀它比任何文档都更快定位问题类型。下一章开始进入第一个真正的机制Inject。为什么 SystemUI 里 1200 处Inject中绝大多数是构造注入而Dependency偏偏用字段注入这个反差本身就是一部迁移史。← 全书目录 | 第 2 章 Inject →

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑