4、Gradle构建生命周期:初始化阶段、配置阶段、执行阶段、生命周期钩子函数

说实话,很多刚接触 Gradle 的开发者,上来就写 task,跑通了就完事。但一旦项目复杂起来,构建慢、行为诡异、插件冲突……这些问题就会找上门来。我当年带团队的时候,有个同事折腾了一整天,就为了搞清楚「为什么这个 task 明明没执行,却报错了?」——嗯,这就是没搞懂生命周期的典型症状。

Gradle 的构建过程,说白了就三个阶段:初始化配置执行。这三个阶段是串行的,每个阶段都有对应的钩子函数,让你能「插一脚」进去做点事情。今天我们就把它彻底讲透。

4.1 三个阶段,一个都不能少

先看一张整体流程图,帮你建立全局认知:

Gradle 构建生命周期三阶段 ① 初始化阶段 确定哪些项目参与构建 解析 settings.gradle 创建 Project 实例 ② 配置阶段 执行 build.gradle 脚本 配置 Task 及其依赖 构建 Task 有向无环图 ③ 执行阶段 按依赖顺序执行 Task 运行 doFirst/doLast 输出构建结果 生命周期钩子函数(可插拔) settingsEvaluated projectsLoaded beforeSettings projectsEvaluated afterEvaluate gradle.buildFinished taskGraph.whenReady buildStarted

4.2 初始化阶段:谁参与构建?

这个阶段只做一件事:确定哪些项目要参与本次构建

Gradle 会先找 settings.gradle(或 settings.gradle.kts)文件。如果找不到,就默认当前目录是单项目构建。找到了,就解析里面的 include 语句,把子项目都注册进来。

我个人习惯在 settings.gradle 里写清楚项目结构,比如:

// settings.gradle
rootProject.name = 'my-app'
include 'core'
include 'web'
include 'api'

初始化阶段结束后,每个项目都会生成一个 Project 对象。注意,这时候 build.gradle 还没执行呢。

小技巧: 如果你在 settings.gradle 里写了 println,它会在初始化阶段就打印出来。可以用来验证阶段顺序。

4.3 配置阶段:构建「任务蓝图」

配置阶段,说白了就是执行所有项目的 build.gradle 脚本。但注意——只是执行脚本,不是执行 task。

什么意思呢?比如你写了一个 task:

// build.gradle
task hello {
    println "我在配置阶段执行了"
    doLast {
        println "我在执行阶段才跑"
    }
}

运行 gradle hello,你会看到:

我在配置阶段执行了
> Task :hello
我在执行阶段才跑

看到了吗?println 写在 task 闭包最外层,它属于配置代码,在配置阶段就跑了。而 doLast 里面的代码,才是真正执行阶段跑的。

我曾经踩过的坑: 刚用 Gradle 时,我把一个耗时的网络请求写在了 task 闭包外层,结果每次运行任何 task 都要等它执行完。后来才发现,配置阶段所有 task 的配置代码都会执行,不管你最终跑哪个 task。所以,耗时的操作一定要放到 doFirst 或 doLast 里。

配置阶段的另一个重要产出是:构建出 Task 的有向无环图(DAG)。Gradle 会根据 dependsOn、mustRunAfter 等依赖关系,计算出 task 的执行顺序。

4.4 执行阶段:真正干活

执行阶段就简单了——Gradle 根据 DAG 图,按顺序执行那些被请求的 task(以及它们的依赖 task)。

每个 task 的执行顺序是:

  1. 执行 doFirst 闭包(如果有多个,按添加顺序正序执行)
  2. 执行 task 本身的 action(比如 Copy 任务的复制操作)
  3. 执行 doLast 闭包(如果有多个,按添加顺序正序执行)

举个例子:

task deploy {
    doFirst {
        println "1. 检查环境"
    }
    doLast {
        println "3. 部署完成"
    }
    doFirst {
        println "0. 前置检查(后添加的 doFirst 反而先执行)"
    }
}

输出顺序是:

0. 前置检查(后添加的 doFirst 反而先执行)
1. 检查环境
3. 部署完成
注意: doFirst 是「后添加先执行」的栈式行为,而 doLast 是「先添加先执行」的队列式行为。这个细节容易搞混,我建议你写代码时保持 doFirst/doLast 的添加顺序清晰,不要交叉添加。

4.5 生命周期钩子函数:在关键时刻「插一脚」

Gradle 提供了丰富的钩子函数,让你能在生命周期的各个节点插入自定义逻辑。我把常用的整理成了表格:

钩子函数 所属阶段 触发时机 典型用途
settingsEvaluated 初始化 settings.gradle 解析完成后 动态修改项目列表
projectsLoaded 初始化→配置 所有 Project 对象创建完成后 对所有项目统一配置
beforeSettings 初始化前 settings.gradle 解析之前 极少用,用于早期干预
projectsEvaluated 配置→执行 所有 build.gradle 执行完成后 检查配置结果、添加额外 task
afterEvaluate 配置→执行 单个项目配置完成后 针对单个项目做后处理
taskGraph.whenReady 配置→执行 Task 依赖图构建完成后 检查最终 task 列表、动态修改
buildFinished 执行后 所有 task 执行完成后 清理资源、发送通知

来看一个实际例子,我在项目中经常用 afterEvaluate 来做一些「事后调整」:

// 在根项目的 build.gradle 中
subprojects {
    afterEvaluate { project ->
        // 检查每个子项目是否配置了 java 插件
        if (project.plugins.hasPlugin('java')) {
            // 自动添加一个自定义 task
            project.task('healthCheck') {
                doLast {
                    println "${project.name} 健康检查通过"
                }
            }
        }
    }
}

为什么用 afterEvaluate?因为这时候子项目的 build.gradle 已经执行完了,插件、依赖、task 都配置好了,你才能安全地检查它们。

我的习惯:gradle.buildFinished 钩子里记录构建耗时,方便排查性能问题。代码很简单:
gradle.buildFinished { result ->
    println "构建结束,耗时: ${result.duration}"
    if (result.failure) {
        println "构建失败: ${result.failure.message}"
    }
}

4.6 避坑指南:配置阶段 vs 执行阶段,别搞混

我见过太多人在这上面翻车了。总结几个常见问题:

  • 问题1: 在配置阶段读取文件内容。文件可能还没生成,读了个寂寞。
  • 问题2: 在 task 闭包外层调用 deletecopy。这些操作会在配置阶段就执行,而不是执行阶段。
  • 问题3:allprojectssubprojects 里写耗时逻辑。配置阶段会遍历所有项目,如果每个项目都跑一遍耗时操作,构建速度直接崩。

我曾经接手过一个项目,构建一次要 8 分钟。排查后发现,有人在 subprojects 里写了个 HTTP 请求,每次配置阶段都去拉远程配置。后来移到 doFirst 里,构建时间降到了 40 秒。嗯,这就是不懂生命周期的代价。

4.7 小结

记住三句话:

  • 初始化阶段:确定谁参与构建,解析 settings.gradle。
  • 配置阶段:执行所有 build.gradle,构建 Task 依赖图。不跑 task 本身。
  • 执行阶段:按图执行被请求的 task。

钩子函数就是你在各个阶段「插一脚」的入口。用好它们,能让你的构建脚本更灵活、更健壮。下次遇到构建行为诡异的问题,先问问自己:这段代码到底在哪个阶段执行?


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