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 评估前后插入逻辑。我个人最常用的是 beforeEvaluateafterEvaluate

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") {
        // 修改配置
    }
}
注意: 不要在 afterEvaluate 里创建新的 Task。虽然技术上可以,但会破坏 Task 执行图的完整性。我曾经这么干过,结果依赖关系乱成一团,调试了一下午。

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 本身也有生命周期钩子:doFirstdoLast。这两个是大家最熟悉的,但我想强调一点:它们的执行顺序是固定的。

tasks.register("myTask") {
    doFirst {
        println("第一个执行")
    }
    doFirst {
        println("第二个执行?不,我才是第一个")
    }
    doLast {
        println("最后执行")
    }
}

注意,doFirst 是后进先出。你注册的多个 doFirst,最后一个注册的反而最先执行。这个细节我吃过亏,当时以为顺序是注册顺序,结果调试了半天。

小技巧: 如果你想让某个逻辑在 Task 执行前无条件执行,用 doFirst。如果想让它在 Task 执行后执行,用 doLast。别把逻辑写在 Task 配置块里,那是在配置阶段执行的,不是执行阶段。

完整的生命周期流程图

下面这张图展示了 Gradle 从启动到结束的完整生命周期。我特意把钩子位置也标出来了,方便你对照。

初始化阶段 配置阶段 执行阶段 beforeEvaluate 执行 build.gradle.kts afterEvaluate Task 图生成 whenReady 钩子 执行 Task(doFirst/doLast) settings.gradle.kts 中注册 build.gradle.kts 中注册

实际项目中的应用

说了这么多理论,来点实际的。我在一个多模块项目中,用生命周期钩子做了两件事:

  1. 统一版本号检查:在 afterEvaluate 中检查所有子项目的版本号是否一致,不一致就报错。
  2. 自动生成文档:在 gradle.taskGraph.whenReady 中判断如果 build Task 被执行,就自动触发文档生成 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 依赖解决的,就别用钩子。

公众号:蓝海资料掘金营,微信 deep3321