第 1 章 依赖注入核心概念

依赖注入(Dependency Injection, DI)是 Android SystemUI 模块化架构的基石。SystemUI 包含几十个子系统(状态栏、导航栏、锁屏、通知、快速设置等),它们之间的依赖关系复杂且运行在同一个长期存在的进程中。没有 DI,手动管理构造参数和生命周期将导致代码耦合高、测试困难、内存泄漏频发。本章深入剖析 DI 的四个核心概念:控制反转、DI 容器、依赖图、作用域定义,并给出在 SystemUI 中的具体应用映射。

1.1 控制反转(IoC)

控制反转是 DI 的根本思想:将对象创建和依赖绑定的控制权从调用方转移到容器或框架。传统模式下,类内部通过 new 直接实例化依赖;IoC 后,类只声明需要什么,由外部注入。

传统 vs IoC 对比

// 传统方式:类内部创建依赖
public class NotificationController {
    private final NotificationManager nm = new NotificationManager();
    private final KeyguardManager km = new KeyguardManager();
}

// IoC 方式:依赖由外部注入
public class NotificationController {
    private final NotificationManager nm;
    private final KeyguardManager km;
    // 构造注入
    public NotificationController(NotificationManager nm, KeyguardManager km) {
        this.nm = nm;
        this.km = km;
    }
}

在 SystemUI 源码(如 SystemUIApplication、SystemUIFactory)中,大量使用 Dagger Hilt 或 Koin 实现 IoC,避免在 onCreate 中手动初始化数十个组件。

1.2 DI 容器

DI 容器是 IoC 的具体实现,负责:

  • 注册依赖的生产方式(工厂、单例、懒加载)。
  • 解析依赖图并自动构造所有对象。
  • 管理实例生命周期(单例、作用域绑定)。

常见 DI 容器包括 Dagger / Hilt(编译时,代码生成)、Koin(运行时,轻量 DSL),以及 Service Locator(简化版容器,但非严格 DI)。在 SystemUI 中,官方推荐使用 Hilt + 自定义组件来细分作用域。

容器类型生成时机性能SystemUI 适用性
Dagger / Hilt编译时高(零反射)适用于生产,代码量大
Koin运行时中(反射/代理)快速原型,调试友好
Service Locator运行时低(全局查找)仅临时构造,不推荐长期使用

1.3 依赖图(Dependency Graph)

依赖图是容器内部所有模块、组件、依赖关系构成的有向图。每个节点代表一个可注入的对象(实例或工厂),每条边代表“依赖关系”。容器在启动时通过拓扑排序构建该图,并检测循环依赖。

SystemUI 依赖图示例(Koin DSL)

val notificationModule = module {
    // 单例:整个应用生命周期只有一份
    single { NotificationManagerService() }
    
    // 工厂:每次注入都创建新实例
    factory { NotificationController(get()) }
    
    // 作用域:绑定到特定子图(例如锁屏作用域)
    scope(named("LOCKSCREEN")) {
        scoped { LockScreenViewController(get()) }
        scoped { LockScreenNotifier(get()) }
    }
}

正确构建依赖图可以避免 饥饿死锁(循环依赖)、重复对象(本应单例却创建多次)、生命周期不一致(例如 Activity 级别对象被 Fragment 级别持有导致泄漏)。

1.4 作用域定义(Scope)

作用域决定了注入对象的存活时间。在 SystemUI 中常见的作用域包括:

  • 单例:进程级别,随 SystemUI 进程启动而创建,进程销毁而销毁(如 NotificationManager、StatusBarManager 的底层服务)。
  • 子图作用域:绑定到特定 UI 或功能模块,例如“锁屏作用域”、“通知面板作用域”、“设置面板作用域”。
  • 瞬时:每次注入都创建新实例,用后即弃。
作用域生命周期典型对象风险
@Singleton进程存活SystemUIApplication、DependencyController内存强引用,需确保不含 Activity 引用
@Scoped(“LockScreen”)锁屏显示到消失LockScreenViewController、KeyguardHostView需要显式关闭作用域,否则内存泄漏
@Factory每次注入时创建Runnable、短生命周期辅助类频繁创建可能导致 GC 抖动

1.5 核心概念关系图

依赖注入核心概念关系 控制反转 (IoC) DI 容器 依赖图 (Graph) 作用域定义 实例生命周期 🔍 依赖图是容器的心脏,作用域是实例的“生存区域” 控制反转让容器接管创建,依赖图定义谁依赖谁,作用域决定每个实例活多久。 SystemUI 中常按子系统划分作用域(如锁屏、通知面板、快速设置)。

1.6 调试与常见错误

  • 循环依赖:容器在构建依赖图时会报错。比如 A → B → A。解法:使用 Lazy 或引入中间层。
  • 作用域泄漏:子图作用域未关闭导致对象被全局引用。SystemUI 中常用 onDismiss() 时销毁作用域。
  • 类型重复:同一个接口有多个实现,未使用 named 或 qualifier 导致注入失败。

性能优化提示:避免在 UI 线程中首次构造重量级依赖(如跨进程 binder)。使用 lazy 或 provider 延迟注入,降低启动延迟。

1.7 在 SystemUI 中的部署结构

典型 SystemUI 模块(基于 Hilt)的目录布局示意:

SystemUI/
├── app/
│   ├── di/
│   │   ├── SystemUIModule.kt          // 全局单例模块
│   │   ├── LockScreenModule.kt        // 锁屏作用域模块
│   │   └── NotificationModule.kt      // 通知作用域模块
│   ├── lockscreen/
│   │   ├── LockScreenController.kt
│   │   └── LockScreenNotifier.kt
│   ├── notification/
│   │   ├── NotificationController.kt
│   │   └── NotificationManager.kt
│   └── SystemUIApplication.kt         // @HiltAndroidApp

每个模块使用 @Module / @InstallIn 注解标明安装到哪个组件,作用域通过 @Scoped 或 @Singleton 定义。