11、生命周期钩子:Gradle生命周期、Project评估前后、Task执行图
说实话,Gradle 的构建过程并不是「啪一下」就完事的。它有一套完整的生命周期。理解这套生命周期,你才能真正掌控构建脚本的执行时机。
我刚开始用 Gradle 时,踩过一个坑:想在某个 Task 执行前自动做一些初始化工作,结果代码写在脚本最前面,Task 还没定义呢,初始化代码就跑完了。后来我才明白——哦,原来 Gradle 是分阶段跑的。
Gradle 的三大阶段
Gradle 的构建过程分为三个阶段:初始化、配置、执行。说白了就是:先确定要构建哪些项目,再读取脚本生成 Task 图,最后按顺序跑 Task。
| 阶段 | 做什么 | 关键点 |
|---|---|---|
| 初始化 | 解析 settings.gradle.kts,确定参与构建的项目 | 决定哪些子项目被包含 |
| 配置 | 执行每个项目的 build.gradle.kts,生成 Task 图 | 所有 Task 对象被创建,依赖关系确定 |
| 执行 | 按依赖顺序执行选中的 Task | 只有被命令行指定的 Task 及其依赖会执行 |
你想想看,配置阶段其实是最容易被误解的。很多人以为写在 build.gradle.kts 里的代码会按顺序执行,其实它们只在配置阶段跑一次。真正干活的代码,得写在 Task 的 action 里。
Project 评估前后的钩子
Gradle 提供了几个钩子,让你能在 Project 评估前后插入逻辑。我个人最常用的是 beforeEvaluate 和 afterEvaluate。
beforeEvaluate
这个钩子在 Project 的 build.gradle.kts 被执行前触发。注意,它只能在父项目或 settings.gradle.kts 中注册,因为子项目还没开始评估呢。
// 在 settings.gradle.kts 中
rootProject {
beforeEvaluate {
println("${project.name} 即将开始评估")
}
}
我在项目中遇到过一种场景:想在子项目评估前统一设置一些属性。比如所有子项目都要用同一个仓库地址,写在 beforeEvaluate 里就很合适。
afterEvaluate
这个钩子更常用。它在 Project 评估完成后触发。这时候所有 Task 都已经定义好了,你可以安全地修改 Task 的配置。
// 在 build.gradle.kts 中
afterEvaluate {
println("${project.name} 评估完成")
// 可以安全地访问所有 Task
tasks.named("compileKotlin") {
// 修改配置
}
}
Task 执行图
配置阶段结束后,Gradle 会生成一张完整的 Task 执行图。这张图是一个有向无环图(DAG),每个 Task 是一个节点,依赖关系是边。
为什么会强调「无环」?因为如果有循环依赖,Gradle 会直接报错。我记得有一次不小心让 Task A 依赖 B,B 又依赖 A,Gradle 直接抛了个 CircularDependencyException。嗯,这种错误其实很好修,但排查起来挺烦的。
Task 执行图的钩子
Gradle 提供了 gradle.taskGraph 对象,让你能在 Task 执行图生成后、Task 执行前做一些事情。
gradle.taskGraph.whenReady { graph ->
println("Task 图已就绪,共有 ${graph.allTasks.size} 个 Task")
// 可以检查哪些 Task 会被执行
graph.allTasks.forEach { task ->
println(" - ${task.path}")
}
}
这个钩子特别适合做「执行前检查」。比如你想确保某个 Task 只在特定条件下才执行,可以在这里判断。
Task 执行前后的钩子
每个 Task 本身也有生命周期钩子:doFirst 和 doLast。这两个是大家最熟悉的,但我想强调一点:它们的执行顺序是固定的。
tasks.register("myTask") {
doFirst {
println("第一个执行")
}
doFirst {
println("第二个执行?不,我才是第一个")
}
doLast {
println("最后执行")
}
}
注意,doFirst 是后进先出。你注册的多个 doFirst,最后一个注册的反而最先执行。这个细节我吃过亏,当时以为顺序是注册顺序,结果调试了半天。
doFirst。如果想让它在 Task 执行后执行,用 doLast。别把逻辑写在 Task 配置块里,那是在配置阶段执行的,不是执行阶段。
完整的生命周期流程图
下面这张图展示了 Gradle 从启动到结束的完整生命周期。我特意把钩子位置也标出来了,方便你对照。
实际项目中的应用
说了这么多理论,来点实际的。我在一个多模块项目中,用生命周期钩子做了两件事:
- 统一版本号检查:在
afterEvaluate中检查所有子项目的版本号是否一致,不一致就报错。 - 自动生成文档:在
gradle.taskGraph.whenReady中判断如果buildTask 被执行,就自动触发文档生成 Task。
// 根项目的 build.gradle.kts
subprojects {
afterEvaluate {
val version = project.version
if (version != rootProject.version) {
throw GradleException("子项目 ${project.name} 版本 $version 与根项目版本 ${rootProject.version} 不一致")
}
}
}
gradle.taskGraph.whenReady {
if (hasTask(":build")) {
// 自动触发文档生成
rootProject.tasks.named("dokkaHtml").get().enabled = true
}
}
- Gradle 生命周期分三阶段:初始化、配置、执行
- beforeEvaluate 在 Project 评估前触发,只能在父项目或 settings 中注册
- afterEvaluate 在 Project 评估后触发,适合修改 Task 配置
- Task 执行图是 DAG,gradle.taskGraph.whenReady 可以访问它
- doFirst 后进先出,doLast 先进先出
嗯,生命周期这块其实不难,但容易忽略。我见过不少同事把代码写在配置块里,以为会在执行阶段跑,结果每次配置阶段都跑一遍,性能白白浪费了。记住:配置阶段的代码只跑一次,执行阶段的代码才跑多次。
最后说一句:用好钩子,能让你的构建脚本更灵活、更健壮。但别滥用,钩子太多会让构建流程变得难以追踪。我个人的原则是:能用 Task 依赖解决的,就别用钩子。