3、Gradle构建生命周期:初始化阶段、配置阶段、执行阶段、生命周期监听回调、project.beforeEvaluate/afterEvaluate实战
说实话,很多Android开发者写了几年Gradle脚本,却从来没搞清楚Gradle到底是怎么运行的。你写了个task,它什么时候执行?你配置了个dependencies,它又是在哪个阶段被解析的?
我刚开始带团队的时候,有个同事跑来问我:“为什么我在build.gradle里写了个打印语句,它每次运行都输出,但我的task却有时候不执行?”
嗯,这个问题其实就指向了Gradle构建生命周期的核心。今天我们就把它彻底讲透。
Gradle构建的三个阶段
Gradle的构建过程,说白了就是三个阶段:初始化、配置、执行。我习惯叫它“ICE模型”,好记。
核心要点:三个阶段是顺序执行的,每个阶段都有对应的监听回调。理解了这个,你就能精准控制脚本的执行时机。
1. 初始化阶段(Initialization)
这个阶段Gradle会做两件事:
- 确定哪些项目参与构建(对于多模块项目)
- 为每个项目创建
Project对象
说白了,Gradle先读settings.gradle,看看你有哪些子模块。然后给每个模块创建一个Project实例。这时候你的build.gradle还没被执行呢。
小技巧:如果你想在初始化阶段干点事,可以在settings.gradle里写代码。比如我经常在这里做版本检查:
// settings.gradle
if (GradleVersion.current() < GradleVersion.version("7.0")) {
throw new GradleException("需要Gradle 7.0+,当前版本:" + GradleVersion.current())
}
2. 配置阶段(Configuration)
这是最容易被误解的阶段。很多人以为build.gradle里的代码就是“执行”的,其实不是——它是在配置阶段被执行的。
配置阶段会:
- 执行每个项目的
build.gradle脚本 - 创建并配置所有的Task对象
- 构建整个Task依赖图
举个例子你就明白了:
// build.gradle
println "这句话在配置阶段就会输出"
task myTask {
println "这个println也在配置阶段执行"
doLast {
println "这个println在执行阶段才执行"
}
}
运行./gradlew myTask,你会看到前两个println先输出,然后才是doLast里的内容。为什么会这样?因为Task的配置代码(花括号里的内容)是在配置阶段执行的,而doLast里的动作是在执行阶段才触发的。
我曾经踩过的坑:在配置阶段执行耗时操作,比如网络请求或文件IO。这会导致每次运行Gradle命令都很慢,哪怕你只是想看个任务列表。记住:配置阶段应该只做配置,不要做真正的“工作”。
3. 执行阶段(Execution)
这个阶段就简单了——Gradle根据你传入的任务名称和依赖关系,按顺序执行那些需要执行的Task。
注意我说的是“需要执行的”。Gradle很聪明,如果某个Task的输入输出没有变化,它就会跳过执行。这就是增量构建的核心机制。
生命周期监听回调
Gradle提供了丰富的生命周期回调,让你能在不同阶段插入自定义逻辑。我整理了一张表:
| 回调方法 | 所属对象 | 触发时机 |
|---|---|---|
settingsEvaluated |
Settings | settings.gradle执行完毕后 |
projectsLoaded |
Gradle | 所有Project对象创建完毕后 |
beforeSettings |
Settings | settings.gradle执行前 |
beforeProject |
Project | 每个Project配置前 |
afterProject |
Project | 每个Project配置后 |
projectsEvaluated |
Gradle | 所有Project配置完毕后 |
buildFinished |
Gradle | 构建完成后(无论成功失败) |
我个人最常用的是projectsEvaluated,因为这时候所有模块的配置都完成了,你可以拿到完整的项目信息来做一些全局操作。
project.beforeEvaluate / afterEvaluate 实战
这两个回调是Project级别的,分别在当前Project配置前后触发。注意:beforeEvaluate只能在settings.gradle或父Project中注册才有效,因为当前Project还没开始配置呢。
来看一个实战场景:
// 在根项目的build.gradle中
subprojects { subProject ->
// 在每个子模块配置前执行
subProject.beforeEvaluate {
println "即将配置模块:${it.name}"
// 可以在这里做一些前置检查
if (!file("${it.projectDir}/src/main/AndroidManifest.xml").exists()) {
throw new GradleException("${it.name} 缺少AndroidManifest.xml")
}
}
// 在每个子模块配置后执行
subProject.afterEvaluate {
println "模块 ${it.name} 配置完成"
// 这里可以读取模块的配置信息
if (it.plugins.hasPlugin('com.android.application')) {
println "${it.name} 是一个Application模块"
// 可以动态修改applicationId之类的
it.android {
defaultConfig {
applicationId "com.example.${it.name}"
}
}
}
}
}
我的经验:afterEvaluate是插件开发中最常用的回调之一。比如你想在所有模块配置完成后,统一检查版本号是否一致,或者给所有模块添加一个公共的Task,这时候afterEvaluate就是最佳时机。
一张图看懂构建生命周期
下面这张SVG图,把整个流程串起来了:
实战:利用生命周期做统一配置
最后分享一个我在实际项目中用的模式。我们有个多模块项目,每个模块都要配置统一的编译选项。以前是每个模块复制粘贴,后来我用afterEvaluate统一处理:
// 根项目的build.gradle
allprojects {
afterEvaluate { project ->
// 只处理Android模块
if (project.hasProperty('android')) {
project.android {
compileSdkVersion 34
buildToolsVersion "34.0.0"
defaultConfig {
minSdkVersion 21
targetSdkVersion 34
versionCode 1
versionName "1.0.0"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
}
// 统一添加一个自定义Task
project.tasks.register('checkModuleHealth') {
doLast {
println "检查模块 ${project.name} 的健康状态..."
// 这里可以做lint检查、依赖分析等
}
}
}
}
}
这样每个子模块的build.gradle就只需要关注自己特有的配置,公共部分由根项目统一管理。维护起来轻松多了。
注意:在afterEvaluate里修改android块的内容时,要确保子模块已经应用了Android插件。否则project.android会报空指针。我建议先检查project.plugins.hasPlugin('com.android.application')或'com.android.library'。
好了,关于Gradle构建生命周期,核心就是这三个阶段和对应的回调。你只要记住:配置阶段只做配置,执行阶段才做实际工作。下次再遇到“脚本执行顺序”的问题,你就能一眼看穿了。