引言
随着 Android 应用规模从几万行增长到几十万行,单模块工程的维护成本呈指数级上升:编译时间暴涨、代码边界模糊、团队协作冲突频发。多模块架构是解决这些问题的必经之路,但错误的模块划分或构建配置同样会让工程陷入"模块地狱"。本文从模块类型设计、API 边界管理、Gradle 增量构建机制、版本对齐策略到诊断工具,系统性地阐述大型 Android 项目的工程化实践。
一、模块类型体系设计
1.1 四大核心模块类型
在成熟的大型 Android 项目中,模块并非随意划分,而是根据职责边界和技术特性形成清晰的类型层级:
app/ ← 应用程序壳(Application Shell)
├── feature/ ← 功能模块(按业务能力垂直拆分)
│ ├── feature-home/
│ ├── feature-profile/
│ ├── feature-search/
│ └── feature-payment/
├── lib/ ← 共享基础库(横向复用)
│ ├── lib-ui/ ← 公共 UI 组件
│ ├── lib-network/ ← 网络层封装
│ ├── lib-database/ ← 数据库层封装
│ ├── lib-analytics/ ← 埋点/分析
│ └── lib-common/ ← 工具类/扩展函数
└── dynamic-feature/ ← 按需下载的功能模块
├── df-wallet/
└── df-live-streaming/
各模块类型的职责定义:
- app:应用入口点,持有 Application 类、Activity 栈,不包含任何业务逻辑,负责组装各个 Feature 模块。
- feature:业务功能的完整封装,包括 UI(Compose/View)、ViewModel、Repository、UseCase,一个 Feature 对应一个完整的用户故事。
- lib:纯技术层封装,无 Android 上下文依赖(或仅依赖轻量上下文),可被任意模块复用。
- dynamic-feature:通过 Play Feature Delivery 实现按需下载,适合用户可选功能(如高级会员功能)。
1.2 Feature 模块的内部结构
一个标准 Feature 模块应遵循 Clean Architecture 的内层依赖原则:
// feature-search 模块目录结构
feature-search/
├── build.gradle.kts
├── src/
│ ├── main/
│ │ ├── kotlin/com/example/feature/search/
│ │ │ ├── di/ ← Hilt 模块,仅声明本模块内的依赖绑定
│ │ │ │ └── SearchModule.kt
│ │ │ ├── data/ ← 数据层
│ │ │ │ ├── remote/ ← DTO、ApiService
│ │ │ │ ├── local/ ← DAO、Entity
│ │ │ │ └── repository/ ← SearchRepositoryImpl
│ │ │ ├── domain/ ← 领域层
│ │ │ │ ├── model/ ← Domain Model(业务概念,不依赖框架)
│ │ │ │ ├── repository/ ← Repository 接口(抽象契约)
│ │ │ │ └── usecase/ ← 业务用例
│ │ │ └── presentation/ ← 表现层
│ │ │ ├── ui/ ← Composable / View
│ │ │ ├── viewmodel/ ← ViewModel
│ │ │ └── SearchContract.kt ← UiState + Intent + Effect
│ │ └── res/
│ │ ├── values/strings.xml
│ │ └── layout/ ← 混合方案:XML 布局片段
│ └── androidTest/ ← Feature 独立测试
│ └── SearchRepositoryTest.kt
关键原则:Feature 模块之间的通信必须通过稳定的公开 API 进行——即 Domain 层的 UseCase 接口。永远不要让一个 Feature 直接依赖另一个 Feature 的 Repository 实现,否则会形成循环依赖或紧耦合陷阱。
1.3 Dynamic Feature 的使用场景
Dynamic Feature(DFD,Dynamic Feature Delivery)通过 Google Play 的 App Bundle 机制实现模块的按需下载。但它并非银弹——DF 模块有约 10MB 的压缩体积上限和额外的首次下载延迟,需要谨慎评估:
// app/build.gradle.kts
android {
bundle {
language {
// 按语言拆分为独立的语言包
enableSplit = true
}
density {
enableSplit = true // 按屏幕密度拆分
}
abi {
enableSplit = true // 按 CPU 架构拆分(arm64-v8a / armeabi-v7a)
}
}
}
// wallet/build.gradle.kts(Dynamic Feature)
android {
// 模块类型声明
bundle {
// 是否在安装时就包含(false = 按需下载)
deliveryTask = ModuleDeliveryTask(ondemand = true)
}
}
// 使用 SplitInstallManager 触发按需加载
class PaymentFragment : Fragment() {
private val splitInstallManager by lazy {
SplitInstallManagerFactory.create(requireContext())
}
private fun installWalletFeature() {
val request = SplitInstallRequest.newBuilder()
.addModule("wallet")
.build()
splitInstallManager.startSplitInstall(request)
.addOnSuccessListener { sessionId ->
// 下载完成,动态注册路由
Router.registerFeature("wallet", WalletEntryPoint())
}
.addOnFailureListener { error ->
Timber.e(error, "Wallet feature download failed")
}
}
}
二、API 边界设计:implementation vs api
2.1 Gradle 依赖变体的本质
理解 implementation 与 api 的区别,本质上是理解 Gradle 的依赖传递图构建策略:
// lib-network/build.gradle.kts
dependencies {
// 场景 A:使用 api(等同于旧版 compile)
// app → feature-search → lib-network → okHttp
// app 和 feature-search 都能访问 okHttp 的类
api("com.squareup.okhttp3:okhttp:4.12.0")
// 场景 B:使用 implementation(推荐默认)
// app → feature-search → lib-network → retrofit
// app 无法访问 retrofit(依赖被"隐藏"了)
implementation("com.squareup.retrofit2:retrofit:2.9.0")
}
// 实际编译图对比:
//
// api 模式(传递暴露):
// app
// ├── feature-search
// │ └── lib-network
// │ ├── okHttp ← app 可访问
// │ └── retrofit ← app 不可访问
// └── lib-network (okHttp 暴露到传递链)
//
// implementation 模式(传递隔离):
// app
// └── feature-search
// └── lib-network
// ├── okHttp ← app 不可访问
// └── retrofit ← app 不可访问
建议原则:lib 模块默认使用 implementation,只在 API 明确需要被下游使用时切换为 api。每次使用 api 前问自己:"这个依赖是否应该成为公共契约的一部分?"
2.2 依赖变体的分层策略
大型项目推荐采用三段式依赖策略,最大化复用率的同时控制编译时间:
// ==============================
// 层级一:Core(最小内核,无任何业务概念)
// ==============================
// lib-common/build.gradle.kts
dependencies {
// 纯 Kotlin 标准库扩展,无 Android 依赖
implementation("org.jetbrains.kotlin:kotlin-stdlib")
implementation("net.ltgt.gradle.incap:incap:0.3") // 注解处理器增强
// 仅允许 api 暴露极稳定的类型
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
// 注意:Coroutines Android 依赖应放在 lib-android-common 中
}
// ==============================
// 层级二:Android 基础库(UI / 框架封装)
// ==============================
// lib-android-common/build.gradle.kts
plugins {
id("com.android.library")
kotlin("android")
kotlin("kapt")
}
android {
namespace = "com.example.lib.android.common"
}
dependencies {
// 隐藏实现细节
implementation(project(":lib-common"))
// Android 框架依赖,隐藏到 lib 内
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
implementation("androidx.compose.ui:ui:1.6.0")
implementation("androidx.compose.material3:material3:1.2.0")
// 如果这些是业务层必须使用的类型,才暴露为 api
api("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
}
// ==============================
// 层级三:Feature 模块(业务封装)
// ==============================
// feature-search/build.gradle.kts
dependencies {
// 只依赖稳定的抽象层,不依赖实现细节
implementation(project(":lib-android-common"))
implementation(project(":lib-network")) // 仅需网络接口
// 依赖注入
implementation("com.google.dagger:hilt-android:2.50")
kapt("com.google.dagger:hilt-compiler:2.50")
}
2.3 版本对齐问题:Diamond 依赖
当多个模块依赖同一个库的不同的版本时,Gradle 默认采用"最新版本覆盖"策略(最新契合法)。这在语义上可能导致运行时崩溃:
// Diamond 依赖冲突示意:
//
// app
// │
// ├── feature-A ──→ lib-X:2.1.0(依赖 OkHttp 4.11)
// │
// └── feature-B ──→ lib-X:1.9.0(依赖 OkHttp 4.9)
//
// Gradle 决议:选择 2.1.0
// 风险:lib-X:1.9.0 的代码可能调用了已被移除的 OkHttp API
// 结果:NoSuchMethodError 或 ClassNotFoundException 在运行时爆发
解决方案是使用 Gradle BOM(Bill of Materials)统一版本仲裁,这在下一节详细展开。
三、增量构建与 Gradle 构建优化
3.1 配置缓存(Configuration Cache)
Gradle 配置阶段(Configuration Phase)是构建性能的最大瓶颈之一——每次构建都会重新计算所有模块的依赖图。配置缓存将配置阶段的输出序列化到磁盘,后续构建直接复用:
// gradle.properties
org.gradle.configuration-cache=true
org.gradle.configuration-cache.problems=warn # 首次启用用 warn,逐步改为 fail
// 在 CI 环境预热配置缓存
// .github/workflows/build.yml
- name: Warm Gradle Configuration Cache
uses: actions/cache@v4
with:
path: |
~/.gradle/caches/transforms-*
~/.gradle/configuration-cache
key: gradle-conf-${{ runner.os }}-${{ hashFiles('**/*.gradle.kts', '**/gradle-wrapper.properties') }}
restore-keys: |
gradle-conf-${{ runner.os }}-
- name: Build with Configuration Cache
run: ./gradlew assembleDebug --configuration-cache
注意:配置缓存对构建脚本有严格要求——所有 build.gradle.kts 中的任务创建必须是幂等的,且不能直接访问 File 系统的时间戳(因为缓存结果会被不同机器复用)。使用 Kotlin DSL 的 providers API(Gradle 8.x+)是配置缓存友好的写法。
3.2 并行任务与任务裁剪
在多模块项目中,合理的并行化可以成倍缩短编译时间:
// gradle.properties
org.gradle.parallel=true // 启用模块间并行
org.gradle.caching=true // 启用任务输出缓存
org.gradle.workers.max=4 // 限制 worker 数量(避免内存溢出)
org.gradle.jvmargs=-Xmx4096m // 充足内存是并行的前提
// 在根 build.gradle.kts 中配置任务图优化
tasks.register("clean", Delete::class) {
delete(rootProject.buildDir)
}
// 增量编译的关键:避免全量编译所有 feature
// 通过任务依赖图分析,让 assembleDebug 只编译必要的模块
tasks.register("assembleCurrentFeature", Task::class) {
val currentFeature = project.findProperty("currentFeature") as String?
dependsOn(":feature-$currentFeature:assembleDebug")
}
// 增量 lint(只 lint 改动的模块)
tasks.register("lintChanged", Lint::class) {
val changedFiles = providers.exec {
workingDir = rootDir
commandLine("git", "diff", "--name-only", "HEAD", "--diff-filter=M")
}.standardOutput.asText.get().split("\n").filter { it.endsWith(".kt") }
changedFiles.forEach { file ->
val containingModule = findModuleForFile(file)
dependsOn(":$containingModule:lint")
}
}
3.3 Kotlin DSL 与 Gradle 脚本的模块化
Kotlin DSL 相比 Groovy DSL 提供了编译期类型检查、IDE 智能提示和重构安全。在大型项目中,应将公共配置抽取为 convention plugins:
// buildSrc/src/main/kotlin/
// Convention Plugin: AndroidLibraryConvention.kt
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.kapt")
}
android {
compileSdk = 34
defaultConfig {
minSdk = 24
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
isCoreLibraryDesugaringEnabled = true
}
kotlinOptions {
freeCompilerArgs += listOf(
"-opt-in=kotlin.RequiresOptIn",
"-Xcontext-receivers" // 实验性功能
)
}
// 统一配置 lint
lint {
warningsAsErrors = true
checkDependencies = true
// 按模块忽略特定的 lint 错误(如迁移期间的兼容性问题)
disable += listOf("ObsoleteSdkInt", "InvokeSingularMethod")
}
}
dependencies {
// 所有 Library 模块的共同依赖
"coreLibraryDesugaring"("com.android.tools:desugar_jdk_libs:2.0.4")
implementation("androidx.core:core-ktx:1.12.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
}
// Feature Convention Plugin: FeatureLibraryConvention.kt
// 继承基础 Library Convention,添加 Feature 特有配置
plugins.apply()
android {
namespace = "com.example.feature.$featureName"
// Feature 模块额外开启的编译器优化
kotlin {
compilerOptions {
freeCompilerArgs.add("-P")
freeCompilerArgs.add("plugin:androidx.compose.compiler.plugins.kotlin:experimentalStrongSkipping=true")
}
}
}
// feature-search/build.gradle.kts
plugins {
id("com.example.android.feature") // 应用自定义 Convention Plugin
}
android {
namespace = "com.example.feature.search"
}
dependencies {
// Feature 模块特殊依赖
implementation(project(":lib-network"))
implementation(project(":lib-ui"))
implementation(project(":lib-common"))
}
四、版本对齐:Gradle BOM 与依赖仲裁
4.1 BOM 的使用与自定义 BOM
Gradle BOM 本身是一个特殊的 POM 文件,它不包含代码,仅声明各依赖的推荐版本。使用 BOM 可以避免 Diamond 依赖冲突,同时减少 version 声明的维护负担:
// gradle/dependencies.toml(Gradle 8.x 新格式,推荐)
[versions]
agp = "8.2.2"
kotlin = "1.9.22"
compose-compiler = "1.5.8"
compose-bom = "2024.02.00"
lifecycle = "2.7.0"
hilt = "2.50"
coroutines = "1.8.0"
retrofit = "2.9.0"
okhttp = "4.12.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidxCoreKtx" }
compose-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "composeBom" }
compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "composeBom" }
// ...
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
// 使用 BOM(所有未指定版本的 androidx.* 依赖使用 BOM 推荐的版本)
dependencies {
// BOM 方式:覆盖所有 androidx.* 的版本
implementation(platform("androidx.compose:compose-bom:2024.02.00"))
androidImplementation(platform("androidx.compose:compose-bom:2024.02.00"))
// 这些库的版本由 BOM 统一控制,无需重复声明版本
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.material3:material3")
implementation("androidx.lifecycle:lifecycle-runtime-ktx")
// 非 BOM 库,单独指定版本
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.google.dagger:hilt-android:2.50")
kapt("com.google.dagger:hilt-compiler:2.50")
}
// 自定义企业级 BOM(用于统一多团队依赖版本)
// version-catalog/bom-catalog.toml
[versions]
# 关键库统一版本管理
network = "2.9.0" # Retrofit 2.9.0
okhttp = "4.12.0"
moshi = "1.15.1"
[libraries]
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "network" }
retrofit-converter = { group = "com.squareup.retrofit2", name = "converter-moshi", version.ref = "network" }
okhttp-core = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
4.2 依赖仲裁与强制版本
当某个间接依赖引入了一个已知不兼容的版本时,使用 constraints 强制覆盖:
dependencies {
// 强制所有 okhttp 间接依赖使用统一版本
constraints {
implementation("com.squareup.okhttp3:okhttp:4.12.0") {
because("4.11 以下版本存在 TLS 握手漏洞 CVE-2023-xxx")
}
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") {
because("必须与 okhttp 主库版本对齐")
}
}
// resolutionStrategy:全局覆盖任意模块的依赖版本
configurations.all {
resolutionStrategy {
// 强制所有配置使用固定的 okhttp 版本
force("com.squareup.okhttp3:okhttp:4.12.0")
force("com.squareup.okhttp3:logging-interceptor:4.12.0")
}
}
}
五、诊断工具
5.1 Build Scan 与构建分析
Gradle Build Scan(--scan)是诊断构建性能问题的终极工具,它将构建过程可视化并上传到 scans.gradle.com 或自建服务器:
# 生成构建扫描报告
./gradlew assembleDebug --scan
# 输出:
# BUILD SUCCESSFUL
# Build scan link: https://gradle.com/s/abc123xyz
# 在 CI 中强制启用并上传到自有服务器
./gradlew assembleRelease \
--scan \
--no-build-cache \
--rerun-tasks \
-Dscan.upload.url=https://gradle.internal.company.com
# 本地查看配置缓存状态
./gradlew --status
# 输出:
# Configuration cache operations: 12 executed, 0 reused
# Task graph size: 47 modules, 312 tasks
Build Scan 报告中重点关注以下指标:
- Configuration Time:配置阶段耗时,目标 < 30s
- Task Execution Time:任务执行时间分布,找出耗时最长的任务
- Task Dependencies:任务依赖图,识别不必要的串行链
- Cache Hit Rate:缓存命中率,分析哪些任务未命中缓存
5.2 dependencyInsight 与依赖树分析
# 查看特定依赖的版本来源
./gradlew :app:dependencies --configuration releaseRuntimeClasspath \
--dependency insight okhttp
# 输出示例:
# com.squareup.okhttp3:okhttp:4.12.0 (forced)
# +--- project :lib-network
# | +--- project :feature-payment
# | | +--- project :feature-search
# \--- project :feature-auth
# \--- com.squareup.okhttp3:okhttp:4.11.0 -> 4.12.0 (forced)
# 强制版本覆盖生效:4.11.0 → 4.12.0
# 查看某模块的完整依赖树(排除测试/AndroidTest)
./gradlew :lib-network:dependencies \
--configuration debugRuntimeClasspath \
--console=plain \
| grep -E "^\+---|^\+---" | head -50
# 分析未使用的传递依赖(清理优化)
./gradlew :app:buildHealth \
--type=unused-dependencies
一个常见的性能陷阱:某个看起来"无害"的传递依赖(如 okio)可能拖慢了 R8/D8 的 DEX 编译速度。使用 dependencyInsight 追踪每个引入 okio 的路径,评估是否可以裁剪:
// build.gradle.kts
// 添加 build health 插件
plugins {
id("com.autonomousapps.analysis") version "2.1.0"
}
// 运行健康度检查
tasks.register("buildHealth", com.autonomousapps.DependencyHealthTask::class) {
analysis = setOf(
Analysis.HELPERS, // 检测未使用的 helper 类
Analysis.ANDROID_WARNINGS,
Analysis.UNUSED_DEPENDENCIES,
Analysis.INCORRECT_DEPENDENCIES,
Analysis.MISUSED_DEPENDENCIES
)
// 生成修复建议
issues = Issues.WARNINGS
}
// 推荐将 build health 检查集成到 CI
// .github/workflows/ci.yml
- name: Dependency Health Check
run: ./gradlew buildHealth
# 失败时生成报告上传到 Artifacts
5.3 构建耗时分析实战
使用 Gradle Profiler 进行受控的性能基准测试:
# build-profile.conf
# Gradle Profiler: ./gradlew --profile 之外的更强大工具
# 安装:brew install gradle-profiler
main {
versions = ["8.5"]
gradle = "8.5"
scenarios {
"clean-assemble" {
tasks = ["clean", "assembleDebug"]
warm-ups = 3
iterations = 5
invocations = 1
cleanup-tasks = ["clean"]
}
"incremental-change" {
tasks = ["assembleDebug"]
warm-ups = 2
iterations = 3
apply-patch = "patches/user-change.patch"
}
}
measured-builds = 3
measured-clean-builds = 3
output = "build-profile-reports"
}
# 运行对比分析
# gradle-profiler --benchmark build-profile.conf
在优化前后运行 Gradle Profiler 对比,可以量化每项优化(如启用配置缓存、切换到 Kotlin DSL、升级 AGP 版本)的实际收益。
六、工程化最佳实践
6.1 模块化成熟度模型
模块化不是一步到位的工程决策。以下是一个可量化的成熟度评估框架:
等级 1 - 单一模块:全部代码在 app 模块中
→ 指标:1 个 build.gradle,编译时间 > 5min
等级 2 - 两层分离:app + lib(共享工具类)
→ 指标:2-3 个模块,lib 不依赖 Android Framework
等级 3 - Feature 拆分:app + lib + features
→ 指标:5-10 个模块,Feature 之间无直接依赖
等级 4 - Clean Architecture:feature 按 data/domain/presentation 分层
→ 指标:10-20 个模块,编译时间 < 3min
等级 5 - Dynamic Feature + 按需加载
→ 指标:> 20 个模块,Play Store App Bundle 开启
等级 6 - 独立模块独立发布(Module as a Library)
→ 指标:每个模块有独立 CI、版本号,可被多个 App 复用
6.2 CI/CD 流水线优化
# .github/workflows/android-ci.yml(GitHub Actions 示例)
name: Android CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
# 级别 1:快速检查(< 3 分钟)
quick-checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { java-version: '17', distribution: 'temurin' }
- name: Gradle Cache
uses: actions/cache@v4
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('*.gradle.kts', 'gradle-wrapper.properties') }}
# 只 lint 改动的模块
- name: Lint Changed Modules
run: ./gradlew lintChanged --no-daemon
- name: Unit Tests (Changed Modules)
run: ./gradlew testChangedUnitTests --no-daemon
# 级别 2:完整 Debug 构建(< 10 分钟,含配置缓存)
full-debug:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { java-version: '17', distribution: 'temurin' }
- name: Build Debug APK
run: ./gradlew assembleDebug --configuration-cache --no-daemon
- name: Upload APK
uses: actions/upload-artifact@v4
with: { name: apk, path: app/build/outputs/apk/debug/*.apk }
# 级别 3:Release 构建 + Benchmarks(仅 main 分支)
release-build:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { java-version: '17', distribution: 'temurin' }
- name: Build Release AAB
run: ./gradlew bundleRelease --configuration-cache --no-daemon
- name: Macrobenchmark
run: ./gradlew :benchmark:macroBenchmark --no-daemon
- name: Upload Baseline Profile
run: ./gradlew uploadBaselineProfile --no-daemon
总结
Android 多模块架构的精髓不在于"拆得多",而在于"拆得对"——按业务边界划分 Feature 模块,通过 Stable API 契约解耦,使用 implementation 隐藏实现细节,配合 Gradle BOM 统一版本仲裁,构建速度可以从数十分钟优化到三分钟以内。配置缓存、并行任务、Build Scan 诊断和 Gradle Profiler 基准测试,构成了完整的构建优化工具链。下一篇文章我们将深入 Kotlin Flow 响应式编程与背压机制,探索协程在生产环境中的最佳实践。