第 3 章:模块边界划分策略——按功能域拆分与循环依赖消除

SystemUI 是 Android 系统中一个长期演进的巨型单体模块,包含了状态栏、导航栏、锁屏、通知面板、快捷设置、媒体卡片等多个视觉与交互区域。当同一模块内混合了不同功能域的代码,便会出现职责不清、依赖混乱、单次修改影响范围失控等问题。按功能域(Functional Domain)划分模块边界 是组件化架构的第一原则——让每个组件只关注一个明确的功能域,并通过依赖规则防止循环引用。

3.1 功能域划分原则

划分边界时,我们遵循 高内聚、低耦合 和 单向依赖 两个核心准则:

  • 高内聚:同一功能域内所有代码应完成一组紧密相关的职责,对外暴露最小化的接口。
  • 低耦合:不同功能域之间仅通过抽象接口或事件总线通信,不直接持有具体实现。
  • 单向依赖:依赖方向必须由相对底层模块指向上层模块,禁止反向引用,形成有向无环图(DAG)。

以 AOSP SystemUI 原始结构为参考,识别出以下 7 个核心功能域:

功能域 主要职责 对外暴露关键接口
StatusBar 显示系统图标、时钟、电池、信号等状态信息 IStatusBarService
NavigationBar 处理三大按键(返回、主页、最近任务)及手势导航 INavigationBar
Keyguard 锁屏界面、解锁流程、安全策略 IKeyguardCallback
Notification 通知列表、通知分组、悬浮通知 INotificationManager
QuickSettings 快捷设置面板、瓦片管理、编辑模式 IQsTileHost
Media 媒体播放器控制、磁贴、锁屏媒体卡片 IMediaControlPanel
SystemUICommon 基础层:主题、动画、工具类、EventBus 定义 无接口,仅被依赖

实践提示: 并非所有模块都直接依赖 Android Framework。Keyguard 需要感知 Notification(例如显示通知在锁屏上),但不要让 Keyguard 直接导入 Notification 的实体类——应通过 INotificationVisibilityProvider 接口解耦。

3.2 循环依赖的典型场景与解耦策略

单体架构中最常见的循环依赖发生在 StatusBar ↔ Notification 以及 Media ↔ Keyguard 之间。例如:

  • 状态栏需要显示通知的图标数目;通知面板又需要知道当前状态栏的可见状态以调整滑动冲突。
  • 锁屏界面需要展示媒体卡片;媒体卡片又需要锁定解锁状态来决定是否显示全屏控制。

解耦方案:提取公共事件总线 + 引入桥接接口。

以下是一个使用 SystemUIEvent 枚举类来消除直接依赖的示例:

// SystemUICommon 模块中定义
public enum SystemUIEvent {
    KEYGUARD_VISIBILITY_CHANGED,
    NOTIFICATION_COUNT_UPDATED,
    MEDIA_SESSION_CHANGED,
    // ……
}

// Notification 模块只发送事件,不关心谁消费
EventBus.getDefault().post(SystemUIEvent.NOTIFICATION_COUNT_UPDATED, count);

// StatusBar 模块只消费事件,不直接引用 Notification
EventBus.getDefault().observe(this, SystemUIEvent.NOTIFICATION_COUNT_UPDATED, count -> updateBadge(count));

注意: 事件总线虽然可以切断编译期依赖,但滥用会导致运行时调试困难。建议将事件总数控制在 20 个以内,并配合接口回调辅助关键业务流(如锁屏解锁通知)。

3.3 依赖规则与层间架构

为了避免模块越权访问,我们定义了三层依赖体系:

  1. 基础层(SystemUICommon):无任何 UI 模块依赖,提供工具、事件定义、通用动画、主题 。
  2. 功能层(各功能域模块):依赖基础层,不依赖其他功能层模块;如果需要跨域协作,必须通过 桥接模块 或 事件。
  3. 宿主层(SystemUIApp):负责依赖注入、模块生命周期管理、配置加载。可以依赖所有功能层和基础层。

下图展示了模块化后的依赖关系全景(无环)。

SystemUI 模块边界划分与依赖关系 SystemUI 模块边界划分与依赖方向(DAG) SystemUIApp 宿主 桥接:EventBus StatusBar NavigationBar Keyguard Notification QuickSettings Media SystemUICommon 基础层 注意:功能域模块之间没有直接箭头,跨域通信统一由桥接模块(EventBus / 接口回调)代理。

3.4 接口定义最佳实践

每个功能域模块应定义 消费者接口(Consumer) 和 提供者接口(Provider),并放在独立的 api 包下,避免实现类泄漏。例如:

// 放在 :systemui:notification:api 模块
public interface INotificationVisibilityProvider {
    boolean shouldShowNotificationOnLockscreen();
    boolean isSensitiveNotification(NotificationEntry entry);
}

// 放在 :systemui:keyguard:api 模块
public interface IKeyguardStateController {
    boolean isShowing();
    boolean isUnlocked();
    void addCallback(IKeyguardCallback callback);
}

关键约束: 每个功能域模块在 build.gradle.kts 中只能依赖 :systemui:common 和其他功能域的 api 模块,不能依赖 impl 模块。Gradle 配置示例如下:

// :systemui:statusbar 模块
dependencies {
    implementation(project(":systemui:common"))
    implementation(project(":systemui:notification:api"))
    // 不允许 implementation(project(":systemui:notification:impl"))
    implementation(project(":systemui:keyguard:api"))
}

3.5 风险点与应对

风险点 说明 应对措施
隐含依赖链过长 间接依赖导致模块 A 在运行时需要模块 C 的类,但编译期看不出来 启用 Gradle dependencies --api 审计;引入 strict-api-only 插件
事件总线泛滥 跨域场景全部使用 EventBus,导致解耦弱、可追踪性差 核心流程(如锁屏解锁、通知点击)使用接口回调,非关键 UI 同步使用事件
模块粒度过细 每个功能域拆成 api/impl/(如 Media 拆分出 3 个子模块),接口爆炸 每个功能域最多拆 2 个模块(api + impl),单个模块代码量 ≤ 5000 行

3.6 验收标准

  1. 构建系统上执行 ./gradlew :systemui:dependencies --configuration compileClasspath,输出必须为无环有向图,且不出现跨模块循环引用(检测工具:Gradle Cycle Check 插件)。
  2. 每个功能域模块的 src/main/java 中只能引用 :systemui:common 或其它模块的 api 包,不得出现任何其它模块的 impl 或内部类。
  3. 跨域接口(INotificationVisibilityProvider 等)的调用链路在运行时可通过 apidump 工具明确查看到来自哪个模块。
  4. 模块独立编译时间不超过 2 分钟(以 2024 年 Pixel 8 编译服务器标准)。

验收自动化: 建议将上述检查配置在 CI 的 check 任务中,例如使用 gradle-cycles-plugin 和 android-module-dependency-audit。出现循环依赖时构建失败并输出模块依赖路径。