0x00. 背景
SystemUI 作为 Android 系统的"门面",管理着状态栏(StatusBar)、通知面板(NotificationPanel)、锁屏(Keyguard)等核心交互,其代码库庞大且状态复杂。为了解耦各个功能模块,Google 在 SystemUI 中深度应用了 Dagger2 进行依赖注入(Dependency Injection, DI)。
在深入 SystemUI 的 GlobalRootComponent 或 SysUIComponent 源码之前,我们需要先对 Dagger2 的核心机制进行一次"高保真"的回顾。本篇不追求面面俱到,而是聚焦于那些支撑 SystemUI 架构的基石概念。
0x01. 核心角色:Dagger2 的注解"五虎将"
Dagger2 的工作本质是通过注解处理器在编译时生成代码,从而构建一个自动化的依赖工厂。理解以下 5 个核心概念,是看懂 SystemUI 源码的前提。
1.1. @Inject:请求与供给的"锚点"
这是最基础的注解,它有两个主要用途:
- 请求依赖:标记在构造函数(Constructor Injection)或成员变量(Field Injection)上,告诉 Dagger:"我需要这个对象。"
- 供给依赖:标记在类的构造函数上,告诉 Dagger:"你可以通过调用这个构造函数来创建我的实例。"
1.2. @Module:生产实例的工厂
并不是所有类都能通过 @Inject 构造函数创建(例如:接口、第三方库的类、需要复杂配置的类)。
- @Module:可以理解为一个"仓库"或"工厂车间",专门用来存放生成对象的方法。
- @Provides :标记在
@Module中的方法上。当 Dagger 需要某个类型的对象,而该对象无法直接构造时,它会来这里寻找带有@Provides的方法。 - @Binds :标记在
@Module中的方法上。当 Dagger 需要找某个接口对应的实现类时使用@Binds进行绑定。
1.3. @Component:连接器(The Bridge)
这是 Dagger 的核心大脑。它是一个接口,连接了"需求方"(使用 @Inject 的地方)和"供给方"(@Module 或 @Inject 构造函数)。
- 它告诉 Dagger 从哪里获取依赖,以及将依赖注入到哪里。
- 在编译时,Dagger 会生成该接口的实现类(如
DaggerCarComponent)。
1.4. @Subcomponent:图谱的继承
在 SystemUI 中,@Subcomponent 至关重要。它用于创建依赖图谱的子图。
- 子组件可以访问父组件的所有对象,但父组件无法访问子组件。
- 应用场景:SystemUI 中有全局单例(Global Scope),也有针对特定用户的会话(User Scope)。当用户切换时,UserSubcomponent 会被销毁并重建,而 GlobalComponent 保持不变。
1.5. @Scope (如 @Singleton):生命周期的管理者
Dagger 默认每次都会创建一个新对象。@Scope 用于将对象的生命周期绑定到 Component 的生命周期上。
- 如果一个 Component 生命周期像 Application 一样长,且对象被标记为
@Singleton,那么该对象就是全局单例。 - 在 SystemUI 中,你会看到大量的
@SysUISingleton,其本质与@Singleton类似,只是为了语义更明确。
0x02. 幕后机制:编译时依赖图构建
不同于 Guice 或 Spring 等框架在运行时 通过反射查找依赖,Dagger2 的魔法发生在编译时(Compile Time) 。
- 注解处理 :编译器扫描所有的
@Inject,@Module,@Component。 - 图谱验证:Dagger 检查是否存在循环依赖、依赖缺失等问题。如果有错,编译直接失败(Fail Fast)。
- 代码生成 :Dagger 生成标准的 Java/Kotlin 代码(如
Factory类和MembersInjector类)。
这意味着,SystemUI 运行时没有任何依赖注入带来的反射性能损耗,这对于对流畅度要求极高的 UI 系统尤为关键。
0x03. 实战演练:一个极简的 Car 组装厂
为了抛开 SystemUI 的复杂业务干扰,我们构建一个干净的汽车模型,展示从定义到注入的完整闭环。
第一步:定义依赖 (The Dependency)
我们定义一个 Engine 接口,以及它的具体实现 V8Engine。注意 V8Engine 拥有 @Inject 构造函数,这意味着 Dagger 知道如何创建它。
Kotlin
// 1. 定义接口
interface Engine {
fun start()
}
// 2. 定义具体实现,并告知 Dagger 如何构造它
class V8Engine @Inject constructor() : Engine {
override fun start() = println("V8 Engine roaring!")
}
// 3. 普通类,可以直接注入
class Wheel @Inject constructor() {
fun roll() = println("Wheel rolling")
}
第二步:定义需求方 (The Consumer)
我们的汽车 (Car) 需要引擎和轮子。即这里定义了"需求方"。
Kotlin
class Car @Inject constructor(
private val engine: Engine, // 需要 Module 提供,告诉 Dagger 这个类是如何获取的
private val wheel: Wheel // Dagger 可以直接通过 @Inject 构造生成
) {
fun drive() {
engine.start()
wheel.roll()
println("Car is moving!")
}
}
第三步:制作模块 (The Module)
这里我们展示 SystemUI 中最常见的 Module 写法:同时包含 @Binds 和 @Provides。
- @Binds (推荐) :用于将接口映射到实现类。它必须是抽象方法 ,且在抽象类 或接口中。它的效率比
@Provides更高,因为 Dagger 不需要实例化 Module,直接调用实现类的 Provider。 - @Provides:用于需要自定义构造逻辑、引入第三方库或基本类型(如 String, Context)的场景。
Kotlin
@Module
abstract class CarModule {
// 【重点】使用 @Binds
// 语义:当有人请求 Engine 接口时,给它 V8Engine 的实例。
// 优势:编译时优化,不生成额外的工厂代码,性能更好。
@Binds
abstract fun bindEngine(impl: V8Engine): Engine
// 使用 @Provides (伴生对象写法是 SystemUI 中的常见模式)
// 场景:如果我们需要提供一些基本类型配置,或者无法直接 @Inject 的对象,例如在使用一些第三方的类库时,可以使用 @Provides 来告诉 Dagger 如何创建具体对象
companion object {
@Provides
fun provideCarName(): String {
return "SystemUI Concept Car"
}
}
}
第四步:组装组件 (The Component)
定义连接器,告诉 Dagger 将 CarModule 纳入版图,并提供获取 Car 的入口。需要使用@Component指定有哪些 Module 类
Kotlin
@Component(modules = [CarModule::class])
interface CarComponent {
// 方式 A:直接获取对象
fun getCar(): Car
// 方式 B:注入到某个容器中(常用于 Activity/Fragment)
fun inject(activity: MainActivity)
}
第五步:最终调用
在代码入口处(类似于 SystemUI 的 SystemUIApplication),构建图谱并使用。
Java
public class MainActivity extends AppCompatActivity {
//请求依赖
@Inject
Car car;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 方式1:使用默认Module
CarComponent component = DaggerCarComponent.create();
// 方式2:自定义Module(更灵活)
CarComponent component2 = DaggerCarComponent.builder()
.carModule(new CarModule())
.ownerModule(new OwnerModule())
.build();
// 在当前页面注入依赖
component.inject(this);
// 现在可以使用依赖了
car.drive();
}
}
输出结果:
Engine V8 Turbo started
Wheel rolling
Car is moving!
小结
通过这个极简示例,我们确立了 Component -> Module -> Dependency 的三角关系。
-
@Inject 定义依赖或者请求依赖
-
@Module 定义Module类,用来告诉 Dagger 如何创建依赖
- @Binds 用于接口与实现类进行绑定
- @Provides 一般用于无法在构造函数中使用@Inject的依赖,例如引用三方类库的依赖时,可以使用
-
@Component 用于组装组件,告诉 Dagger 有哪些 Module 类
0x04. Hilt 的崛起:应用开发的利器
在现代 Android 开发中,Google 强烈推荐使用 Hilt。它是建立在 Dagger2 之上的一层封装库。对于普通 App 开发者,Hilt 是救星,但对于 SystemUI 工程师,它是"另一个世界的产物"。
4.1 为什么要用 Hilt?
原生 Dagger2 功能强大,但有一个致命痛点:样板代码过多且难以标准化 。你需要手动定义 Component,手动在 Application 中实例化它,再手动写 inject() 方法。对于专注业务的应用开发者来说上手难度是比较大的。
Hilt 的出现就是为了解决这个问题。它提供了一套 "标准化的组件层级"。
Hilt 的核心优势:
- 开箱即用 :预定义好了
SingletonComponent,ActivityComponent,ViewModelComponent等标准容器。 - 自动注入 :你不再需要写
component.inject(this),只需一个注解。
Hilt 的核心注解:
@HiltAndroidApp:应用入口点
- 角色 :标记在 Application 类上。
- 作用 :它触发了 Hilt 的代码生成过程。在编译时,它生成一个继承自该 Application 的基类,并自动创建和配置应用的 根组件 (即 Dagger2 中的
AppComponent),Hilt 称之为SingletonComponent。此组件的生命周期与 Application 进程生命周期一致。
@AndroidEntryPoint:开启注入大门
- 角色 :标记在任何标准 Android 组件(
Activity,Fragment,Service,View,BroadcastReceiver)上。 - 作用 :告诉 Hilt,该组件的实例需要进行依赖注入。在编译时,Hilt 会生成一个与该组件关联的 Dagger Component(如
ActivityComponent或FragmentComponent),并自动调用注入方法(告别手动component.inject(this))
@InstallIn:定义作用域边界
- 角色 :标记在
@Module上。 - 作用 :强制定义该 Module 中的依赖(
@Provides或@Binds方法)将安装到哪个预定义的 Hilt Component 中。这是实现作用域管理的关键。
@HiltViewModel:Jetpack ViewModel 专用
- 角色 :标记在
ViewModel子类上。 - 作用 :允许 Hilt 自动注入
ViewModel的构造函数依赖,并确保ViewModel能够从正确的 Component(通常是ActivityRetainedComponent或FragmentComponent)中获取依赖
Hilt 代码极简示例:
Kotlin
// 1. 在Application类添加 @HiltApplication 注解
@HiltApplication
class MyApplication : Application() {
}
// 2. 只需要加上 @AndroidEntryPoint,Hilt 自动处理注入逻辑
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var car: Car // 直接可用
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
car.drive()
}
}
// 3. 需要告诉 Hilt 把这个 Module 安装到哪个标准容器里
@Module
@InstallIn(SingletonComponent::class) // <--- Hilt 独有:指定安装位置
abstract class CarModule {
@Binds
abstract fun bindEngine(impl: V8Engine): Engine
}
4.2 Dagger vs. Hilt:本质区别
| 特性 | Dagger 2 (原生) | Hilt |
|---|---|---|
| 定位 | 通用 Java/Kotlin 依赖注入框架 | 专为 Android 优化的 Dagger 封装 |
| Component | 手动定义:你需要自己写 Interface,自己决定层级关系 | 预定义:提供标准 Android 生命周期组件 (Application, Activity, Fragment) |
| 灵活性 | 极高:你可以构建任意形状的依赖图 | 受限:强制遵循 Hilt 的标准层级结构 |
| 上手难度 | 高 (陡峭的学习曲线) | 低 (注解驱动,傻瓜式) |
4.3 灵魂拷问:为什么 SystemUI 不使用 Hilt?
既然 Hilt 这么好用,为什么作为 Google 亲儿子的 SystemUI 依然坚持使用原生 Dagger2,甚至写了大量的样板代码?
这是阅读源码时必须理解的架构背景:
-
历史包袱与迁移成本 SystemUI 的代码库历史远早于 Hilt 的诞生。它拥有数以千计的类和庞大的依赖图谱。将这样一个巨型单体架构迁移到 Hilt,不仅工作量巨大,而且风险极高。SystemUI 目前仍处于从旧的
Dependency.get(Class)静态查找模式向 Dagger 迁移的过程中,引入 Hilt 会增加额外的复杂度。 -
非标准的生命周期与上下文 Hilt 是为标准 Android App 设计的,它假设你的世界由
Application->Activity->Fragment组成。 但 SystemUI 不是一个普通的 App。它包含:- DreamService (屏保)
- Keyguard (锁屏,独立于 Activity 生命周期)
- Quick Settings Tiles (快捷设置磁贴)
- SystemUI Process (常驻系统进程) 这些组件的生命周期非常特殊,Hilt 预定义的
ActivityComponent或FragmentComponent很难完美覆盖 SystemUI 中千奇百怪的窗口和服务的需求。
-
多用户架构 (Multi-User Support) 这是最关键的技术原因。SystemUI 必须在设备层面(Global)和用户层面(User-Specific)之间有严格的界限。
- 当你在 Android 上通过"多用户"功能切换用户时,SystemUI 的一部分组件必须保持不变(如状态栏图标管理),而另一部分必须销毁并重建(如与当前用户设置绑定的组件)。
- SystemUI 使用原生 Dagger 的
@Subcomponent手动构建了复杂的 UserScope 机制。原生 Dagger 允许开发者精确控制何时创建子图、何时销毁子图。而 Hilt 的自动化机制在处理这种"系统级用户切换"的动态图谱时,显得不够灵活且难以控制。
4.4 结论
SystemUI 选择原生 Dagger2,是因为它需要绝对的控制权。它需要根据系统特有的生命周期(如 User Switch, Boot Complete)来手动管理依赖图谱的构建与销毁,这是为了系统稳定性所做的必要权衡。
在下一部分中,我们将离开舒适区,进入 SystemUI 的庞大代码库,看看 Google 工程师是如何利用这些基础积木,搭建起管理 Android 系统的摩天大楼的。