Gradle 模块化构建:多 module 工程结构、模块间依赖规则、compileOnly 与 implementation 的选用

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 落地步骤

  1. 架构预审:基于架构图,产出模块清单和依赖矩阵,标注每个模块的允许依赖类型(api/implementation/compileOnly)。
  2. 创建空模块:使用 Android Studio 的 New Module 向导或手动创建 settings.gradle.kts 条目,每个模块只包含空的 build.gradle.kts 和 src/main 目录。
  3. 定义依赖配置:按照 4.2 节的推荐表逐步添加依赖,完成后运行 ./gradlew :app:systemui:dependencies 验证依赖树无反向环。
  4. 依赖验证 CI 任务:在 CI pipeline 中添加自定义 Gradle Task,校验是否存在禁止的依赖路径(例如 :feature:* → :feature:*)。
  5. 增量构建基线测试:对单个模块修改后测量 --build-cache 冷/热启动时间,记录基线。

4.5.2 验收标准

  • 无循环依赖:./gradlew -p app dependencies --cyclic(需安卓插件 8.0+ 支持)输出为空。
  • API 泄漏检测:对 :core:* 模块运行 closeGuarded 规则,确保未在公共类中 import feature 层的类型。
  • 编译 cache 命中率:在开发者日常修改场景下,cache 命中率 > 80%(增量编译 < 10秒)。
  • APK 瘦身:compileOnly 替换后,APK 中不包含注解处理器、编译器和测试框架的类(通过 apkanalyzer 验证)。

4.6 模块依赖架构图(SVG)

以下 SVG 图展示了 SystemUI 组件化工程中核心模块的依赖方向与配置类型。箭头表示 implementation / api 依赖,虚线框表示 compileOnly 边界。

SystemUI 模块依赖图 :app:systemui (壳) :feature:statusbar :feature:nav** :feature:qs** :sfx:notifications-api :sfx:systemevent-api :sfx:settings-api :core:common :core:annotations :core:bridge-impl 虚线箭头 = compileOnly 依赖 实线箭头 = implementation/api 依赖

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 信任链路检查清单

在每一次模块拆分或依赖变更后,对照以下清单检查信任链路的完整性:

  1. 依赖方向:是否出现 :feature:xxx 依赖 :app:systemui?若是,必须打断。
  2. 接口泄漏::core:annotations 中是否出现了 Android SDK 类(如 android.app.Activity)?若是,应移回 :core:bridge-impl。
  3. 测试代码依赖:testImplementation 中是否误用了 implementation,导致测试库随主工程发布?
  4. 循环依赖预防:settings.gradle.kts 中的模块引用是否形成了 A → B → C → A?使用 gradle dependencies --cyclic 自动检测。
落地提醒:建议团队引入 dependency-check Gradle 插件,在每次合并 MR 前自动校验模块依赖矩阵与架构定义是否一致。可以将禁止的依赖路径写入 allowedDependencies.yml 文件中,由 CI Pipeline 拒绝不符合规则的构建。