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 依赖规则与层间架构
为了避免模块越权访问,我们定义了三层依赖体系:
- 基础层(SystemUICommon):无任何 UI 模块依赖,提供工具、事件定义、通用动画、主题 。
- 功能层(各功能域模块):依赖基础层,不依赖其他功能层模块;如果需要跨域协作,必须通过 桥接模块 或 事件。
- 宿主层(SystemUIApp):负责依赖注入、模块生命周期管理、配置加载。可以依赖所有功能层和基础层。
下图展示了模块化后的依赖关系全景(无环)。
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 验收标准
- 构建系统上执行
./gradlew :systemui:dependencies --configuration compileClasspath,输出必须为无环有向图,且不出现跨模块循环引用(检测工具:Gradle Cycle Check 插件)。 - 每个功能域模块的
src/main/java中只能引用:systemui:common或其它模块的api包,不得出现任何其它模块的impl或内部类。 - 跨域接口(INotificationVisibilityProvider 等)的调用链路在运行时可通过
apidump工具明确查看到来自哪个模块。 - 模块独立编译时间不超过 2 分钟(以 2024 年 Pixel 8 编译服务器标准)。
验收自动化: 建议将上述检查配置在 CI 的 check 任务中,例如使用 gradle-cycles-plugin 和 android-module-dependency-audit。出现循环依赖时构建失败并输出模块依赖路径。