Gradle Wrapper 工作原理与多模块工程结构设计
Gradle Wrapper 不是一个方便少打一行命令的小脚本,而是 Android 工程构建环境的“引导加载器”。
当你执行 ./gradlew assembleDebug 时,真正先运行的不是本机已经安装的 Gradle,而是仓库里的 Wrapper 脚本。它读取 gradle/wrapper/gradle-wrapper.properties,确认这个项目声明的 Gradle 发行版、下载地址和校验信息,然后把对应版本解压到 Gradle User Home,再由这个被锁定的 Gradle 版本接管后续构建。
可以把 Wrapper 想象成一把工程专用钥匙。团队成员、CI 机器和发布流水线拿到同一把钥匙,就会打开同一套 Gradle 引擎;如果每个人都用自己机器上的全局 Gradle,就像每个人拿不同型号的扳手修同一台机器,迟早会出现“我这里能构建、你那里不能构建”的环境漂移。
本文承接前两篇对 Gradle 生命周期与 DSL 的分析,进入工程落地层:Wrapper 如何保证构建入口一致,多模块目录如何映射到 Settings 与 Project 模型,以及 Android 大型工程为什么必须把构建逻辑从业务模块里拆出去。
Wrapper 的启动链路
一个标准 Wrapper 至少包含四类文件:
.
├── gradlew
├── gradlew.bat
└── gradle/
└── wrapper/
├── gradle-wrapper.jar
└── gradle-wrapper.properties
执行链路可以简化成:
./gradlew
|
|-- 设置 JVM 参数、定位 Java
v
gradle-wrapper.jar
|
|-- 读取 gradle-wrapper.properties
|-- 校验 distributionUrl / distributionSha256Sum
|-- 下载并解压 Gradle distribution
v
~/.gradle/wrapper/dists/gradle-x.y-bin/...
|
`-- 启动真正的 Gradle Main
Wrapper 的关键点在于“项目声明构建工具版本”。distributionUrl 决定 Gradle 版本,distributionSha256Sum 用来防止下载产物被篡改。对 Android 项目来说,这个版本不是随便升级的:Android Gradle Plugin 和 Gradle 之间存在兼容矩阵,AGP 升级、Gradle 升级、JDK 升级必须一起验证。
为什么不能依赖全局 Gradle
全局 Gradle 的最大问题不是“旧”,而是不可控。
开发者 A: Gradle 8.8 + JDK 17
开发者 B: Gradle 9.0 + JDK 21
CI : Gradle 8.6 + JDK 17
这三套环境可能解析不同插件、触发不同弃用检查、启用不同缓存行为。构建脚本里一个看似普通的 API,在某个版本只是 warning,在另一个版本可能已经变成 error。
Wrapper 把这一层不确定性收束到仓库中:
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionSha256Sum=...
工程实践中,gradlew、gradlew.bat、gradle-wrapper.jar、gradle-wrapper.properties 都应该提交到版本库。升级 Wrapper 应使用 ./gradlew wrapper --gradle-version ...,而不是手工改一处 URL 就结束,因为脚本本身也可能随 Gradle 版本演进。
settings.gradle 是项目拓扑入口
Wrapper 启动 Gradle 之后,Gradle 的初始化阶段会读取 settings.gradle(.kts)。这里决定“这个构建包含哪些 Project”。
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "ZeroBugAndroid"
include(":app")
include(":core:network")
include(":feature:article")
include(":core:network") 不是在磁盘上“发现一个目录”,而是在构建模型里创建一个逻辑路径为 :core:network 的 ProjectDescriptor。默认情况下它映射到 core/network 目录,但你也可以显式调整 projectDir。
这解释了多模块工程的一个底层约束:目录结构、Gradle project path、包名、发布坐标是四套不同命名系统。它们最好保持一致,但不要混为一谈。
| 层次 | 示例 | 决定什么 |
|---|---|---|
| 磁盘目录 | core/network |
文件组织和 IDE 展示 |
| Project path | :core:network |
Gradle 任务路径与项目依赖 |
| Namespace | club.zerobug.core.network |
Android R 类、Manifest、源码命名空间 |
| Maven coordinate | club.zerobug:network |
发布到仓库后的组件坐标 |
多模块不是把文件夹切碎
Android 多模块的目的不是让项目看起来更“工程化”,而是把构建图拆成可复用、可缓存、可并行的子图。
一个合理的工程结构通常长这样:
.
├── app/
├── core/
│ ├── common/
│ ├── database/
│ └── network/
├── feature/
│ ├── home/
│ └── article/
├── build-logic/
└── gradle/
├── libs.versions.toml
└── wrapper/
其中:
app负责最终应用装配,不应该沉淀大量业务实现。feature/*以用户可感知的业务能力切分。core/*承载跨 feature 的基础能力,但不能反向依赖 feature。build-logic承载构建约定,避免每个模块复制 Android/Kotlin 配置。gradle/libs.versions.toml承载依赖别名和版本声明。
判断一个模块切分是否健康,不看模块数量,而看依赖方向:
app
|
+--> feature:article
| |
| +--> core:network
| `--> core:database
|
`--> core:common
如果 core 反向依赖 feature,或者多个 feature 之间互相依赖,构建图会越来越像一团线。表面问题是循环依赖,深层问题是领域边界已经失效。
build-logic 比 buildSrc 更适合大型工程
Gradle 历史上常用 buildSrc 复用构建逻辑。buildSrc 的优点是零配置:只要存在,它就会被自动编译并加入所有构建脚本 classpath。问题也在这里:它对整个构建是隐式全局输入,里面任何改动都可能导致大量脚本重新编译。
更稳妥的做法是使用 included build 承载 convention plugins:
// settings.gradle.kts
pluginManagement {
includeBuild("build-logic")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
build-logic/
├── settings.gradle.kts
├── build.gradle.kts
└── src/main/kotlin/
├── zerobug.android.application.gradle.kts
├── zerobug.android.library.gradle.kts
└── zerobug.kotlin.library.gradle.kts
业务模块只表达自己的意图:
plugins {
id("zerobug.android.library")
}
这背后的设计思想是:业务模块不应该知道“compileSdk、Java toolchain、lint、testOptions、Kotlin compilerOptions 到底怎么配”。它只选择自己是什么类型的模块,公共构建约定由插件注入。
工程结构的可维护边界
一个成熟 Android 工程的根目录应该把职责拆开:
| 位置 | 职责 | 不应该放什么 |
|---|---|---|
settings.gradle.kts |
项目拓扑、插件仓库、依赖仓库策略 | 业务模块细节 |
根 build.gradle.kts |
根项目级插件声明、极少量全局任务 | 各模块 Android 配置 |
build-logic |
可测试、可复用的构建约定 | 业务源码 |
gradle/libs.versions.toml |
依赖别名和版本声明 | 复杂解析规则 |
app |
应用装配、入口、发布配置 | 通用基础设施 |
feature/* |
业务能力闭环 | 全局工具箱 |
core/* |
中立基础能力 | 反向依赖业务 |
如果一个配置需要复制到十几个模块,优先抽到 convention plugin;如果一个依赖版本被多个模块引用,优先进入 Version Catalog;如果一个依赖版本必须参与冲突仲裁,优先使用 platform/BOM 或 constraints,而不是只改 catalog 版本。
Wrapper 与工程结构共同保证可复现构建
可复现构建不是“我今天能跑通”,而是几个月后、另一台机器、另一个 CI job 仍然能用同样输入得到可信结果。
Wrapper 锁住构建工具入口,多模块结构约束构建图形状,convention plugin 收敛配置分叉,Version Catalog 和 BOM 管住依赖入口。它们共同完成的是同一件事:把构建从个人机器上的偶然状态,变成仓库可以描述、可以审查、可以回滚的工程事实。