4. Gradle模块化构建
SystemUI 的组件化解构在代码层面落地为多 module 工程。Gradle 构建脚本承担着定义模块边界、控制依赖可见性、优化编译增量与最终产物大小的多重职责。本章聚焦于如何通过 Gradle 模块化构建,将架构设计映射为可维护、可测试、可独立迭代的构建单元。
4.1 多 module 工程结构:模块边界与系统分层
工程结构直接反映架构分层。典型 SystemUI 组件的 Gradle 模块树按照“层-功能-变体”进行组织,每一层有明确的依赖方向。
| 层级 | 模块名称示例 | 职责边界 | 对外可见性 |
|---|---|---|---|
| 壳工程 | :app:systemui |
Application 入口、资源 overlay、Dagger 顶级组件 | 仅存在于最终 APK |
| 核心层 | :core:common |
基础工具、通用常量、纯数据类(POJO) | 所有模块可见 |
| 框架抽象层 | :sfx:notifications-api |
接口定义、契约类、跨功能使用的抽象 | 仅上游功能模块可见 |
| 功能特性层 | :feature:statusbar |
状态栏、导航栏、通知面板等具体业务实现 | 依赖注入时暴露最小接口 |
| 扩展/变体层 | :variant:device-override |
OEM 定制、Android Go 差异、平板适配 | 仅在特定 build variant 下引入 |
feature-statusbar)而非驼峰,Gradle 会将其自动转换为路径友好的名称,同时避免与 Java/Kotlin 包名混淆。
4.2 模块间依赖规则:控制流与信任链路的构建映射
模块依赖规则必须与架构的“信任链路”对齐——依赖方向即信任方向。依赖允许反向意味着架构退化。
4.2.1 禁止反向依赖与循环依赖
所有模块必须遵循“上层可依赖下层,下层绝不依赖上层”。定义模块依赖时的常见约束包括:
- 核心层(
:core:*) 不得依赖任何:feature:*或:sfx:*模块。 - 框架抽象层(
:sfx:*) 只能依赖:core:*,且接口定义中不引入 Android Framework 类(除非是 AIDL 生成的接口)。 - 功能特性层(
:feature:*) 可以依赖多个:sfx:*和:core:*,但同一层不同 feature 模块之间禁止直接依赖——跨功能交互必须通过:sfx:*层的接口或事件总线。
4.2.2 依赖配置推荐表
| 场景 | 推荐依赖配置 | 理由 |
|---|---|---|
| 引用另一个模块的接口定义 | implementation(project(":sfx:notifications-api")) |
编译期必须包含实现,运行时由 Dagger 注入 |
| 仅编译期使用,运行时不需传递 | compileOnly(project(":core:annotations")) |
生成代码后不再需要,减少 APK 体积 |
| 模块内部实现的公共工具 | api(project(":core:common")) |
需要被该模块的消费者显式可见 |
| 测试框架与 mock 库 | testImplementation(...) / androidTestImplementation(...) |
只在测试时存在,不污染主工程构建 |
| 第三方 SDK 接入层 | compileOnly(...) + 运行时条件加载 |
避免未安装 SDK 的设备崩溃,降低构建耦合 |
api:当模块 A 使用 api 依赖模块 B,所有直接或间接依赖 A 的模块都会在编译时看到 B 的类型。这会导致:编译范围膨胀、意外符号冲突、模块边界变得模糊。仅当 A 的公共 API 签名中暴露了 B 的类型时,才应使用 api,否则一律用 implementation。
4.3 compileOnly 与 implementation 的选用:接口隔离与编译优化
compileOnly 与 implementation 的选用直接决定模块之间的“编译边界”和“传输边界”。在 SystemUI 组件化场景中,正确选用这两者可带来显著的构建加速和更干净的类隔离。
4.3.1 场景一:编译时注解处理器
例如 Dagger、AutoService 或自定义注解处理器。注解处理类仅在编译期需要,最终产出是生成的源代码,它们不应出现在运行时 classpath 或 APK 中。
// Dagger 编译时注解
compileOnly("com.google.dagger:dagger-compiler:2.x")
// Dagger 运行时仍需 implementation
implementation("com.google.dagger:dagger:2.x")
4.3.2 场景二:跨模块接口的“桩模块”
当功能模块 A 需要访问功能模块 B 的某个接口,但出于信任链路约束只能通过抽象层 :sfx:xxx-api,可将 B 的实现作为 compileOnly 依赖引入 A 的测试代码(testCompileOnly)或特定 build variant 下,以避免在正常构建中泄露 B 的实现细节。
4.3.3 场景三:系统隐藏 API 的桥接
SystemUI 常需使用 Android 内部隐藏 API(如 @hide 或 @SystemApi)。这些 API 在不同等级的设备上存在性不同。推荐将其封装到 :core:bridge-impl 模块并用 compileOnly 暴露给消费者,运行时通过反射或 ServiceManager 按需调用。
// 在 :core:bridge-impl 中
compileOnly(project(":core:bridge-api"))
// 在 :feature:statusbar 中
implementation(project(":core:bridge-impl")) // 获得桥接实现
// 但 bridge-impl 内部的隐藏 API 调用不在上层暴露
4.4 数据流/控制流在构建层的体现
Gradle 模块依赖配置本身也是一种“控制流”——它决定了编译顺序和资源可见性。每个模块的 build.gradle.kts 可以视为一份“信任清单”:dependencies 块中列出的模块是编译期和运行期都可信的;未列出的模块不应被 import,也不应被 R 资源引用。
:feature:statusbar 需要获取通知数据,其数据流为:SystemSensorManager (系统服务) → :core:common (数据包装) → :sfx:notifications-api (接口定义) → :feature:statusbar (UI 消费)。相应的构建依赖链为:
:feature:statusbar → implementation(:sfx:notifications-api) → implementation(:core:common)。箭头方向与控制流完全一致。
4.5 落地步骤与验收标准
4.5.1 落地步骤
- 架构预审:基于架构图,产出模块清单和依赖矩阵,标注每个模块的允许依赖类型(api/implementation/compileOnly)。
- 创建空模块:使用 Android Studio 的 New Module 向导或手动创建
settings.gradle.kts条目,每个模块只包含空的build.gradle.kts和src/main目录。 - 定义依赖配置:按照 4.2 节的推荐表逐步添加依赖,完成后运行
./gradlew :app:systemui:dependencies验证依赖树无反向环。 - 依赖验证 CI 任务:在 CI pipeline 中添加自定义 Gradle Task,校验是否存在禁止的依赖路径(例如
:feature:*→:feature:*)。 - 增量构建基线测试:对单个模块修改后测量
--build-cache冷/热启动时间,记录基线。
4.5.2 验收标准
- 无循环依赖:
./gradlew -p app dependencies --cyclic(需安卓插件 8.0+ 支持)输出为空。 - API 泄漏检测:对
:core:*模块运行closeGuarded规则,确保未在公共类中 importfeature层的类型。 - 编译 cache 命中率:在开发者日常修改场景下,cache 命中率 > 80%(增量编译 < 10秒)。
- APK 瘦身:
compileOnly替换后,APK 中不包含注解处理器、编译器和测试框架的类(通过apkanalyzer验证)。
4.6 模块依赖架构图(SVG)
以下 SVG 图展示了 SystemUI 组件化工程中核心模块的依赖方向与配置类型。箭头表示 implementation / api 依赖,虚线框表示 compileOnly 边界。
4.7 选型权衡与风险点
| 决策项 | 方案 A | 方案 B | 推荐场景与风险 |
|---|---|---|---|
| 模块粒度 | 细粒度(每个widget一个模块) | 中粒度(按 feature 聚合) | 细粒度导致 settings.gradle.kts 膨胀和跨模块 diff 过多;推荐中粒度,仅在接口变更多时拆分为 :sfx 级别 |
| 依赖传递性 | 全用 implementation |
混合 api 与 implementation |
全 implementation 可能导致 public 类型在模块外不可见,增大重构阻力;核心基础库(:core:common)应保留 api |
| 版本对齐 | 通过 versionCatalog 统一 |
每个模块单独定义 | 单独定义会造成依赖版本碎片化,排查编译冲突困难;强制使用 versionCatalog 并由 CI 检查一致性 |
| 编译加速 | 模块内预编译 + Gradle 配置缓存 | 不开启配置缓存 | 配置缓存在大规模 module 中可减少 30%~50% 配置时间,但需注意 task 序列化兼容性,遇问题时可回退 |
4.8 信任链路检查清单
在每一次模块拆分或依赖变更后,对照以下清单检查信任链路的完整性:
- 依赖方向:是否出现
:feature:xxx依赖:app:systemui?若是,必须打断。 - 接口泄漏:
:core:annotations中是否出现了 Android SDK 类(如android.app.Activity)?若是,应移回:core:bridge-impl。 - 测试代码依赖:
testImplementation中是否误用了implementation,导致测试库随主工程发布? - 循环依赖预防:
settings.gradle.kts中的模块引用是否形成了A → B → C → A?使用gradle dependencies --cyclic自动检测。
dependency-check Gradle 插件,在每次合并 MR 前自动校验模块依赖矩阵与架构定义是否一致。可以将禁止的依赖路径写入 allowedDependencies.yml 文件中,由 CI Pipeline 拒绝不符合规则的构建。