Appearance
gradle-lifecycle-demo
1. 实验目标与工程要点
用最小 Gradle 工程(一个 settings.gradle.kts + 一个 build.gradle.kts,零插件、零三方依赖)验证 Gradle 三阶段生命周期的回调输出顺序:
- Initialization:读取
settings.gradle.kts,确定参与构建的 project 集合; - Configuration:求值每个 project 的 build script 顶层代码,注册任务,产出 Task Graph(
beforeProject/afterProject/taskGraph.whenReady都在这一阶段或其末尾触发); - Execution:按拓扑序真正执行被选中的 task(
doLast),最后buildFinished。
掌握的工程决策:
- 为什么改一行
build.gradle.kts连增量构建都会变慢——顶层代码属于 Configuration,每次构建都重新求值; - 「任务注册」与「任务执行」是两个阶段的两个动作——
tasks.register在配置期、doLast在执行期; - 排查「我的 println 为什么没打印 / 打印了两次」时,先判断它写在哪个阶段的作用域里。
2. 深入原理与生活浅显比喻
2.1 生活浅显比喻
Gradle 构建像一场婚宴筹备:
- Initialization = 确定宾客名单(读
settings.gradle.kts,决定哪些 module 上桌); - Configuration = 每桌确认菜单与上菜顺序(求值所有 build script、排出任务依赖图)——这一步即使最后只上一道菜也要全桌走一遍;
- Execution = 后厨按顺序真正做菜上菜(跑
doLast)。
2.2 底层原理分析
- Gradle 把一次构建拆成三阶段,是为了先收集完整信息再行动:只有所有 build script 都求值完(Configuration 结束),才能得到完整的任务依赖图,再按拓扑排序执行。
gradle.beforeProject {}/gradle.afterProject {}是 project 配置生命周期钩子;gradle.taskGraph.whenReady {}是配置与执行的分界线;gradle.buildFinished {}在构建收尾触发。它们都注册在Gradle对象上,可以在 settings script 或 init script 中注册。- Kotlin DSL 的
tasks.register(x) { ... }返回TaskProvider,闭包在任务被实例化/执行前不会运行;而tasks.create(x)会立即实例化。本 lab 全部使用register+doLast来凸显两阶段差异。
2.3 方案对比矩阵
| 方案 | 通俗比喻 | 底层原理 | 保证特性 | 吞吐性能 | 运行开销 | 适用场景 |
|---|---|---|---|---|---|---|
| 本 lab:settings/build 脚本内嵌回调 | 婚宴流程广播 | Gradle 对象上的生命周期监听器 | 输出顺序即真实阶段顺序 | 单次小工程构建 | 极低(几条 println) | 学习/验证阶段语义 |
init script 注入(-I hook.gradle.kts) | 空降观察员随行记录 | 同一套监听器,但不动宿主脚本 | 零侵入,任意工程可注入 | 低 | 低 | 观测既有大工程(见 Android host 版) |
Build Scanner / --profile | 赛后录像分析 | 构建数据序列化上报 | 时间轴+瓶颈定位 | 中 | 中 | 性能调优而非机制理解 |
「吞吐性能」与「运行开销」分两栏,遵守仓库规范。
3. Android / 移动端实战场景与误配事故
3.1 实战场景
Android 工程里同样的回调常用于:
kotlin
// 根 build.gradle.kts:统计每个 module 的配置耗时
gradle.afterProject {
if (project.path == ":app) {
logger.lifecycle(app 模块配置完成)
}
}AGP 的全部 Android 任务(mergeDebugResources、compileDebugKotlin…)都在 Configuration 阶段注册进任务图——所以模块越多配置越慢,这也是 Configuration Cache 存在的意义。
3.2 常见误配与事故注意事项
- 把执行期逻辑写进配置期:build script 顶层直接
exec { }/ 读网络 / 解析大文件,导致每次构建(哪怕gradle help)都付出这份开销,且 incompatible with Configuration Cache。 tasks.createvstasks.register混用:create在配置期就实例化任务并求值其配置闭包,破坏惰性;大工程会拖慢 Configuration。- 在
afterEvaluate里又注册afterEvaluate:嵌套回调时机难以推理,容易产生「第二次构建才生效」的诡异现象。
3.3 排障思路
- 现象:「println 打印了但我没跑那个 task」→ 它写在了 Configuration 作用域(build script 顶层或任务配置块),属正常行为;
- 现象:「task 里改了输入却显示 UP-TO-DATE」→ 改的是执行期局部变量,未进
inputs/outputs,Gradle 认为输入未变; - 先看输出顺序:初始化输出 → 配置输出 → 任务行(
> Task :hello)→ 收尾输出,任何乱序说明代码放错了作用域。
4. 实验源码与运行验证
关键源码
- settings.gradle.kts:Initialization 顶层输出 + 四个跨阶段生命周期回调;
- build.gradle.kts:Configuration 顶层输出 +
register/doLast两阶段对比。
下面输出对应以上两份源码。
关键参数 / 开关
gradle hello:只执行hello一个任务,用于观察「goodbye 已注册但未执行」;--console=plain:去掉进度条等富文本干扰,让阶段输出可逐行对照;-q(quiet):反向对照——quiet 下println仍会输出(它不走 logger),而logger.lifecycle会被吞掉,可用于区分两种输出通道。
运行方式
环境要求:本机需安装 Gradle 8.x(或任意含 Kotlin DSL 支持的 7.x+)与 JDK 17+,无需 Android SDK、无需联网拉依赖(本 lab 零插件零依赖)。当前编写该 lab 的机器未安装 gradle,故以静态自检兜底;有 Gradle 的机器请实跑并把真实输出回填到下节。
bash
cd labs/ci/gradle-lifecycle-demo
node syntax-check.mjs # 无 gradle 环境:括号配对 + 回调结构静态自检
gradle hello --console=plain # 有 gradle 环境:真实三阶段输出预期控制台运行输出
gradle hello --console=plain 的预期输出(首次运行会有下载 wrapper/daemon 噪音,以下为稳态形态):
text
阶段1-初始化:读取 settings.gradle.kts,rootProject=gradle-lifecycle-demo
阶段2-配置开始:beforeProject 回调 -> project「root project 'gradle-lifecycle-demo'」
阶段2-配置:build.gradle.kts 顶层代码求值(任何 task 都会触发我)
阶段2-配置结束:hello / goodbye 均已注册进任务容器(注册发生在配置期,执行发生在执行期)
阶段2-配置结束:afterProject 回调 -> project「root project 'gradle-lifecycle-demo'」,此时 build script 已全部求值、任务已注册
阶段2→3 分界:taskGraph.whenReady —— 任务图就绪,共 2 个任务待执行
> Task :hello
阶段3-执行:hello 的 doLast 正在运行(只有被选中的任务才会到这里)
阶段3-执行结束:buildFinished 回调,构建结果=成功
BUILD SUCCESSFUL
1 actionable task: 1 executednode syntax-check.mjs 在无 Gradle 环境下的真实输出:
text
✔ 存在 settings.gradle.kts
✔ 存在 build.gradle.kts
✔ 存在 README.md
✔ settings.gradle.kts 括号配对完整
✔ build.gradle.kts 括号配对完整
✔ settings.gradle.kts 注册了 beforeProject
✔ settings.gradle.kts 注册了 afterProject
✔ settings.gradle.kts 注册了 taskGraph.whenReady
✔ settings.gradle.kts 注册了 buildFinished
✔ settings.gradle.kts 声明了 rootProject.name
✔ build.gradle.kts 惰性注册了 hello 任务
✔ hello/goodbye 使用 doLast 表达执行期行为
全部自检通过(仅静态检查;真实三阶段输出仍需 Gradle 环境验证)预期现象与观察重点
- 预期现象:输出严格按「初始化 → 配置 →
> Task :hello→ buildFinished」排列;gradle help时配置段照样打印,但没有任何> Task :hello执行行。 - 观察重点:
goodbye在配置期已注册(顶层 println 可证),但gradle hello时它的doLast从不运行;taskGraph.whenReady报告的任务数包含未执行的goodbye——「入选任务图」≠「被执行」;- 第二次运行时任务可能显示
UP-TO-DATE,但配置段输出依旧逐行重跑——这就是 Configuration Cache 要消除的部分。
5. 对应知识库文档
- 理论主文档:Gradle 构建生命周期与 AGP 任务链:从配置到产物
- 同族实验(init script 零侵入注入版,Android host 承载):gradle-lifecycle-hook