2. 构建生命周期:初始化、配置、执行三个阶段;生命周期钩子函数详解
Gradle 的构建过程,说白了就是一场精心编排的三幕剧。很多人用 Gradle 好几年,却从来没关心过构建到底是怎么一步步走下来的。结果出了问题,比如某个配置明明写了却不起作用、Task 执行顺序乱套,只能靠瞎猜去排查。
我个人习惯把 Gradle 构建的生命周期拆成三个阶段来理解:初始化(Initialization)、配置(Configuration)、执行(Execution)。记住这个骨架,你就能看懂大部分构建问题了。
2.1 初始化阶段:谁该参与这次构建?
当你敲下 gradlew build 的时候,Gradle 要做的第一件事不是直接去编译代码,而是先搞清楚——这次构建涉及哪些项目? 这就是初始化阶段干的事。
- 查找并解析 settings.gradle:Gradle 从当前目录向上寻找
settings.gradle。如果找不到,就默认单项目构建;找到了,就根据里面的include声明确定要引入的子项目。 - 创建 Gradle 实例和 Project 对象:每个被包含的项目都会生成一个
Project对象,但此时它们还没加载 build.gradle 中的配置。 - 应用初始化脚本:位于
~/.gradle/init.d/或项目根目录下的init.gradle会在这个阶段执行。
apply from: 加载一个子模块的配置——那是配置阶段的事。
2.2 配置阶段:Task 是怎么被记住的?
配置阶段的核心工作就是:为每个 Project 执行 build.gradle 脚本,把里面声明的 Task、依赖、插件全部解析成内存中的对象图。
注意,这里有一个很容易踩的坑:所有 Task 的 doLast、doFirst 闭包里的代码不会在当前阶段执行,而是被注册到 Task 的执行队列中。 所以配置阶段不要写耗时操作,它会拖慢整个构建启动时间。
// 错误示范:在配置阶段打印日志,会每次都打印
tasks.register("hello") {
println "Hello" // 配置阶段执行
doLast {
println "World" // 执行阶段执行
}
}
在这个阶段,Gradle 会构建出一个完整的 Task 依赖有向无环图(DAG)。当你使用 dependsOn 或 mustRunAfter 时,Gradle 会自动拓扑排序,保证执行顺序正确。
2.3 执行阶段:让 Task 真正跑起来
配置阶段结束后,Gradle 会选出本次构建要执行的 Task 集合(比如你执行 gradlew build,它会找出名为 build 的 Task 及其所有依赖),然后按照 DAG 的顺序依次执行每个 Task 的 actions(也就是 doFirst / doLast 闭包)。
执行阶段的特点:
- 增量构建:如果 Task 的输入/输出没有变化,Gradle 会跳过它(UP-TO-DATE)。
- 并行执行:通过
--parallel参数可以让无依赖的 Task 并发运行。 - 构建缓存:缓存命中时直接从缓存获取结果,连执行都不需要。
build.gradle 的顶层写了个 new XmlParser().parse(...)。每次配置阶段都会解析,而项目有 20 多个模块,等于重复解析了 20 多次。后来我把它挪到 Task 的 doLast 里,配置阶段瞬间变快。
2.4 生命周期钩子函数详解
Gradle 在三个阶段的关键节点暴露了一些钩子(Hook),让我们有机会嵌入自定义代码。别小看这些钩子,很多高级优化技巧(比如动态修改 Task、条件化配置)都依赖它们。
| 钩子名称 | 触发时机 | 所属阶段 | 常用场景 |
|---|---|---|---|
settingsEvaluated | settings.gradle 解析完成 | 初始化阶段 | 读取 settings 中的属性,动态添加项目 |
projectsLoaded | 所有 Project 对象已创建 | 初始化 → 配置过渡 | 对所有项目统一配置一些默认行为 |
beforeEvaluate | 每个 Project 开始执行 build.gradle 前 | 配置阶段 | 为项目注入额外的属性或依赖 |
afterEvaluate | 每个 Project 的 build.gradle 执行完毕 | 配置阶段 | 修改已配置的 Task,或者添加额外条件 |
taskGraph.whenReady | Task 依赖图构建完成,但尚未执行 | 配置阶段末尾 | 检查最终依赖图,调整 Task 执行顺序或跳过条件 |
buildFinished | 所有 Task 执行完毕(成功或失败) | 执行阶段结束后 | 汇总构建报告、清理临时资源、发送通知 |
下面是一个实战中常用的钩子组合示例,用来做多模块项目的统一配置:
// settings.gradle
gradle.settingsEvaluated { settings ->
println "settings 已加载,include 的项目: ${settings.rootProject.name}"
}
// root build.gradle
gradle.projectsLoaded {
println "所有子项目对象已创建"
}
subprojects {
// 配置阶段开始前
project.beforeEvaluate { proj ->
println "即将配置 ${proj.name}"
}
// 配置阶段结束后
project.afterEvaluate { proj ->
println "${proj.name} 配置完成"
// 这里可以动态修改 Task,比如统一添加清理任务
}
}
gradle.taskGraph.whenReady { graph ->
// 检查是否包含某个 Task
if (graph.hasTask(':app:assembleRelease')) {
// 比如增加一个安全检查
println "发布构建,运行额外检查"
}
}
gradle.buildFinished { result ->
// 发送构建结果
println "构建耗时: ${result.duration}"
}
2.5 避坑指南
- 不要在配置阶段打印大量日志:配置阶段每执行一次 build 脚本都会跑一遍,打印太多会影响启动速度。用
logger.info控制级别。 - afterEvaluate 里不要做耗时操作:它虽然是配置阶段,但多个 Project 的 afterEvaluate 是顺序执行的,一个卡住就是连锁阻塞。
- 谨慎使用 settingsEvaluated 修改项目列表:如果你在 settingsEvaluated 里动态增加
include项目,Gradle 会报错——因为项目解析已经结束了。正确的做法是在include之前用条件判断。