构建生命周期:初始化、配置、执行三阶段详解
Gradle 的构建过程,说白了就是跑三个固定的阶段。很多人写脚本时把代码都塞在一个 doLast 里完事,其实根本不知道 Gradle 在背后帮你干了多少事。
我第一次深入理解生命周期,是因为一个线上问题——配置阶段里跑了一个网络请求,导致每次执行 gradle tasks 都要等十几秒。嗯,那之后我才认真把这三个阶段掰扯清楚。
核心认知:Gradle 构建不是一股脑执行所有代码,而是分为初始化、配置、执行三个阶段。每个阶段的代码执行时机、作用域完全不同。
三阶段概览
| 阶段 | 作用 | 执行频率 | 典型耗时 |
|---|---|---|---|
| 初始化 | 确定参与构建的项目,创建 Project 对象 | 每次构建 1 次 | 秒级 |
| 配置 | 解析 build.gradle,构建 Task 依赖图 | 每次构建 1 次 | 秒~十秒级 |
| 执行 | 按依赖顺序执行选定 Task 的 actions | 按需 | 取决于任务量 |
阶段一:初始化 (Initialization)
这个阶段最容易被忽视。说白了,Gradle 要先搞清楚「这个项目到底有多大」。它从根目录开始寻找 settings.gradle 文件,然后决定哪些模块要参与构建。
具体流程:
- 寻找并解析 settings.gradle 文件
- 根据 include 语句创建 Project 对象
- 确定根项目是谁,子项目有哪些
⚠ 注意:如果 settings.gradle 中有语法错误,构建会在初始化阶段直接失败,不会进入后续阶段。
初始化阶段可以用的钩子:
- settingsEvaluated —— settings.gradle 解析完毕
- projectsLoaded —— 所有 Project 对象已创建
阶段二:配置 (Configuration)
这是我最想提醒大家注意的阶段。配置阶段会执行每个项目的 build.gradle 文件,构建出完整的 Task 依赖图。
我之前在一个大型 Android 项目里,发现构建要等 20 秒才开始跑任务。一查,配置阶段里有个插件在做大量反射扫描。我建议你把耗时的操作尽量延迟到执行阶段。
配置阶段做的事:
- 执行 build.gradle 中的脚本语句
- 注册所有 Task 对象
- 构建 Task 依赖网络
- 调用 afterEvaluate 等配置钩子
// 配置阶段执行的代码 —— 每次构建都会跑
tasks.register('myTask') {
// 这里的代码在配置阶段执行
println "我在配置阶段就被执行了"
doLast {
// 这里的代码在执行阶段执行
println "我直到执行阶段才会跑"
}
}
常用钩子一览:
| 钩子名称 | 触发时机 | 使用场景 |
|---|---|---|
| beforeEvaluate | 项目开始配置之前 | 预设置默认属性 |
| afterEvaluate | 项目配置完毕之后 | 修改最终配置、添加依赖 |
| gradle.projectsEvaluated | 所有项目配置完毕 | 跨模块配置检查 |
💡 个人习惯:我在 afterEvaluate 里做跨模块的依赖检测,比如确保 common 模块的版本号在所有子模块中一致。
阶段三:执行 (Execution)
执行阶段才是真正干活的阶段。只有这个阶段会运行 Task 的 actions,也就是 doFirst、doLast 里的逻辑。
执行阶段的规则:
- 只会执行用户指定的 Task(即命令行里输入的编译/测试等)
- 按照 Task 依赖顺序执行
- 如果一个 Task 没有被依赖或者没有被指定,它就不会执行
// 观察三阶段执行顺序
tasks.register('showOrder') {
doFirst {
println "执行阶段 —— doFirst"
}
doLast {
println "执行阶段 —— doLast"
}
println "配置阶段 —— task 注册时"
}
// 输出结果:
// 配置阶段 —— task 注册时
// 执行阶段 —— doFirst
// 执行阶段 —— doLast
⚠ 注意:千万不要在配置阶段写耗时逻辑!我曾经见过有人在 build.gradle 里直接执行 git 命令获取版本号,每次构建都多等 5 秒。正确做法是放到执行阶段或者用 Provider 延迟计算。
生命周期钩子实战
掌握了三阶段,我们就能精准地在正确的时间点插入逻辑。
常用的 Project 级别钩子:
// 在项目配置开始前 → 初始化阶段最有用
gradle.beforeProject {
println "即将配置项目:${it.name}"
}
// 在项目配置结束后 → 配置阶段最常用
afterEvaluate {
println "${project.name} 配置完毕,task 数量: ${tasks.size()}"
}
// 所有项目配置完成 → 跨模块整合
gradle.projectsEvaluated {
println "所有子项目都配置好了,可以检查依赖一致性了"
}
Task 级别钩子:
// 为某个特定 task 添加前置动作
tasks.named('compileJava') {
doFirst {
println "开始编译 Java 代码..."
}
doLast {
println "编译完成,输出目录: ${destinationDir}"
}
}
💡 避坑经验:我曾经在 doFirst 里做文件清理,结果因为 task 被缓存跳过了,清理代码根本没执行。后来我把文件清理放到独立的 task 里,并通过 dependsOn 与编译 task 关联,才解决了问题。
总结:三阶段记忆口诀
| 阶段 | 口诀 | 关键钩子 |
|---|---|---|
| 初始化 | 先确定项目范围 | settingsEvaluated、projectsLoaded |
| 配置 | 画好任务依赖图 | afterEvaluate、projectsEvaluated |
| 执行 | 按图逐个跑任务 | doFirst、doLast |
搞清楚这三个阶段,你写的 Gradle 脚本会少很多「玄学问题」。以后遇到构建卡顿,先判断是配置阶段耗时还是执行阶段耗时,方向就对了。