Gradle 构建基础
Gradle 是 Android 项目的默认构建系统。很多朋友刚开始接触时,只觉得它是个“配置文件堆砌”的工具,build.gradle 里的东西照着官方文档抄就行了。但我在做模块化 CI 的那段时间发现——不了解 Gradle 的核心机制,你连编译为什么慢、依赖为什么冲突都搞不清楚。
说白了,Gradle 就是一个基于 Groovy(或 Kotlin)的构建框架。它定义了三件事:有什么(Project)、做什么(Task)、用什么(Plugin)。整个构建过程就是按剧本把 Task 串起来跑一遍。
2.1 构建生命周期:三个阶段
Gradle 的执行流程分成三个大阶段。我记得刚入职时,看到构建日志里那么多“Configured”和“Executing”,完全分不清先后。后来自己写了个自定义插件才彻底搞明白。
| 阶段 | 工作内容 | 典型事件 |
|---|---|---|
| Initialization | 解析 settings.gradle,确定哪些 Project 参与构建 | include ':app', ':library' |
| Configuration | 解析每个 Project 的 build.gradle,创建 Project 对象和所有 Task | apply plugin, dependencies, defaultConfig |
| Execution | 根据用户输入的任务名称,找到依赖图谱并依次执行 | ./gradlew assembleDebug |
我曾犯过的错: 在配置阶段做耗时操作(比如网络请求版本号)。Gradle 的配置阶段是单线程执行的,而且不论最终执行哪个 Task,所有 Project 的配置阶段都会跑一遍。你想想看,一个多模块项目,配置阶段可能就要花几十秒。
所以经验是:不要把耗时的计算或 I/O 放到配置阶段。用 doLast 或 gradle.taskGraph.whenReady 推迟到执行阶段。
2.2 核心元素:Project、Task、Plugin
每个 build.gradle 文件对应一个 Project 对象。Project 是 Gradle 的顶层抽象,你写的所有配置本质上就是在操作这个对象。
Task 是真正干活的单元。编译、打包、签名、lint,每个动作都是一个 Task。我习惯把 Task 比喻成“流水线上的操作工人”。
Plugin 则是“插件包”,它帮你注册一堆 Task 和扩展 DSL。比如 com.android.application 就帮你生成了 assemble、compile 等几十个 Task。
// 一个简单的自定义 Task
task hello {
doLast {
println 'Hello from Gradle!'
}
}
你可以在命令行 ./gradlew hello 执行它。Task 之间可以依赖:
task taskA {
doLast { println 'A' }
}
task taskB(dependsOn: taskA) {
doLast { println 'B' }
}
运行 taskB 时会先执行 taskA。这就是 Task 依赖图的基本原理。Gradle 会分析整个依赖图,确保每个 Task 只执行一次,且按正确的顺序。
2.3 构建脚本的核心配置
除了 Task,日常打交道最多的就是 dependencies、android、plugins 这些 DSL 块。它们本质上都是 Project 对象的扩展属性。
| 配置块 | 作用 | 常见写法 |
|---|---|---|
| plugins | 声明插件(推荐新写法) | id 'com.android.application' version '8.2.0' |
| android | AGP 的配置扩展 | compileSdk, defaultConfig, buildTypes |
| dependencies | 声明依赖 | implementation, api, annotationProcessor |
| ext | 自定义扩展属性 | ext.kotlinVersion = '1.9.0' |
个人习惯: 我通常在根项目的 build.gradle 里用 ext 定义版本号,然后在子模块里用 rootProject.ext.kotlinVersion 引用。这样升级版本时只改一处就可以了。
注意 implementation 和 api 的区别。如果你写了一个 library 模块,implementation 会让依赖仅对当前模块可见,而 api 会传递到依赖该模块的消费者。我在项目中遇到过因为误用 api 导致依赖版本冲突的坑——最终用了 implementation 解决问题。
2.4 多项目构建:模块化的基石
模块化项目需要在 settings.gradle 里声明子模块:
rootProject.name = 'MyApp'
include ':app'
include ':library:core'
include ':library:network'
每个子模块都有自己的 build.gradle。你可以用 subprojects 块给所有子模块统一配置:
subprojects {
apply plugin: 'java-library'
// 公共配置...
}
注意顺序:allprojects 包括根项目,subprojects 只包括子模块。别搞混了。
2.5 属性管理与 Gradle 缓存
gradle.properties 文件是放全局配置的好地方。比如:
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
android.useAndroidX=true
android.enableJetifier=true
我还喜欢在这里放一些构建开关:
IS_CI=false
ENABLE_R8=true
然后在 build.gradle 里通过 project.property('IS_CI') 读取。这样就能在 CI 环境和本地开发环境切换不同的配置了。
关于 Gradle 缓存,默认开启了构建缓存(Build Cache)。但要注意:缓存 key 是基于 Task 输入和输出计算的。如果 Task 声明了不稳定的输入(比如时间戳),缓存就失效了。我曾经因为一个自定义 Task 里忘了声明 outputs.upToDateWhen,导致每次构建都重新运行——白白浪费了几分钟。
关键总结:
- Gradle 构建三阶段:Initialization → Configuration → Execution
- Project 是核心,Task 是执行单元,Plugin 是功能包
- 任务依赖图决定执行顺序,请确保无环
- 多模块项目用 settings.gradle 声明,subprojects 做公共配置
- gradle.properties 和 ext 是管理版本和开关的好方法
- 注意缓存失效问题,尤其是自定义 Task
这张图展示了 Gradle 的构建三阶段与 Project、Task、Plugin 的关系。看似简单的流程,实际踩过的坑可能不少。比如配置阶段如果写死了时间依赖,或者 Task 之间形成了环形依赖,Gradle 就会报错甚至死循环。我在刚接触模块化时,就因为两个模块互相声明了 implementation project 导致配置阶段直接报异常——排查了一下午才发现是循环引用。
构建基础就聊到这里。接下来你要投入实战,很快就会发现:理解了这些底层概念,才能看懂 AGP 版本适配和 CI 流水线里的各种“魔法”到底是怎么工作的。