Appearance
Gradle 构建生命周期与 AGP 任务链:从配置到产物
回到总览:构建与发布工程相关模块:Flutter 产物瘦身与 AOT 拆包 · Flutter JIT/AOT 编译模式
一句话定义
Gradle 构建分 Initialization → Configuration → Execution 三阶段;Android 构建 = Gradle 生命周期 × AGP 注册的任务链(assembleDebug 等)。理解它才能解释"为什么改一行依赖要重配全工程"以及"Configuration Cache / Build Cache 为什么能加速"。
代码索引
| 主题 | Lab 说明 | 源码 |
|---|---|---|
| Gradle 三阶段生命周期回调输出 | gradle-lifecycle-demo(独立轻量 lab,零插件零依赖;另有 Android host 注入版 gradle-lifecycle-hook) | settings.gradle.kts · build.gradle.kts |
为什么需要
- 为什么改一行
build.gradle.kts后增量构建也常变慢?- 一句话答:Configuration 阶段会重新执行全部 build script,改动使配置输入失效、缓存失效;用 Configuration Cache 缓存配置结果可避免重复配置(详见下文 §底层机制.3)。
- 为什么
implementation与api用错会导致"下游模块编译报找不到类"?- 一句话答:
implementation不把传递依赖暴露给消费方编译期可见,消费方若直接引用该类型会编译失败;api才会在消费方编译 classpath 上保留。真正的运行期 NoClassDefFoundError 多来自compileOnly误用(见「相近概念对比」)。
- 一句话答:
- 为什么
assembleDebug一次会触发几十个任务,而不是直接打包?- 一句话答:AGP 在 Configuration 阶段注册了一条依赖任务图,
assembleDebug只是叶子节点,Gradle 按依赖顺序先跑资源合并、编译、dex、打包等上游任务(见 §底层机制.2)。
- 一句话答:AGP 在 Configuration 阶段注册了一条依赖任务图,
底层机制
1. 三阶段生命周期与 hook
对应 Lab:gradle-lifecycle-demo —— 用最小 Gradle 工程复演下方回调输出顺序(
beforeSettings/beforeProject/afterProject/taskGraph.whenReady/buildFinished),并对照「注册在配置期、执行在执行期」的register+doLast差异。
kotlin
// 在 settings.gradle.kts 或 init script 中注册生命周期回调
gradle.beforeSettings { println("阶段1-初始化:读取 settings,建立 project 集合") }
gradle.beforeProject { println("阶段2-配置:${it.name}") }
gradle.taskGraph.whenReady { println("阶段2结束:任务图就绪,共 ${it.allTasks.size} 个任务") }
gradle.buildFinished { println("阶段3-执行结束") }- 可能执行顺序
阶段1-初始化:读取 settings,建立 project 集合阶段2-配置:app(每个 project 依次回调)阶段2结束:任务图就绪,共 N 个任务- 任务逐个执行(如
> Task :app:compileDebugKotlin) 阶段3-执行结束
- 可能输出
阶段1-初始化:读取 settings,建立 project 集合 阶段2-配置:app 阶段2结束:任务图就绪,共 53 个任务 > Task :app:compileDebugKotlin UP-TO-DATE > Task :app:packageDebug 阶段3-执行结束 - 预期现象:Initialization 最先执行且只跑一次;Configuration 对每个 project 各回调一次,是所有
build.gradle.kts真正被求值的阶段;Execution 才产生产物。改一行依赖通常只让 Configuration 失效,但 Configuration Cache 命中时可跳过整段配置。 - 观察重点:连续两次无改动构建时,若
compileDebugKotlin显示UP-TO-DATE而非重新执行,说明任务级增量缓存生效;若每次都重跑配置回调,说明未启用 Configuration Cache 或配置输入不稳定。
2. AGP 任务链(assembleDebug 向下展开)
AGP(com.android.application 插件)在 Configuration 阶段向 Task Graph 注入 Android 专属任务,assembleDebug 仅是最末的聚合任务:
bash
# 只打印任务图,不真正执行,用来确认 assembleDebug 的依赖展开
./gradlew :app:assembleDebug --dry-run --console=plain- 可能输出
> Task :app:processDebugResources > Task :app:compileDebugKotlin > Task :app:dexBuilderDebug > Task :app:packageDebug > Task :app:assembleDebug - 预期现象:执行顺序是拓扑排序结果,编译类任务先于 dex,dex 先于打包;任一上游失败则
assembleDebug不会执行。 - 观察重点:
--dry-run只打印任务图不执行,是确认依赖关系最安全的手段。
3. 三类缓存:Configuration Cache / Build Cache / Daemon
- Configuration Cache:把 Configuration 阶段产出的任务图与任务输入输出快照序列化缓存,下次构建若配置输入未变则直接复用任务图,跳过全部 build script 求值。解决"配置慢"。
- Build Cache:缓存任务输出(本地 + 远端),跨机器/分支复用,解决"重复编译产物"。
- Gradle Daemon:常驻 JVM,避免每次构建重启 JVM 与类加载开销;与上面两者正交。注意它和 Kotlin Compile Daemon 是两回事——Kotlin 编译器跑在独立进程里,且只从 Gradle Daemon 继承 3 个 JVM 参数,调 Kotlin 编译要写
kotlin.daemon.jvmargs(详见 Kotlin 编译守护进程、jvmargs 继承与 K2)。
Android / Flutter / Web / Backend 对照
| 维度 | Android (Gradle/AGP) | Flutter (Dart) | Web (Webpack/Vite) | Backend (Maven/Gradle) |
|---|---|---|---|---|
| 配置阶段 | Configuration 执行 build.gradle.kts | 无独立配置阶段,pub get 解析依赖 | Webpack 插件在 compiler 前静态分析 | pom.xml / build.gradle 解析 |
| 产物 | APK / AAB | libapp.so(AOT) / kernel(DJIT) | bundle / chunk | jar / war |
| 增量机制 | up-to-date 检查 + Build Cache | 热重载(DJIT) | HMR / 模块级重建 | 插件增量 |
| 加速手段 | Configuration Cache | Dart AOT 树摇 | esbuild/Rollup 预构建 | 并行构建 |
常见场景
- 多模块工程:
settings.gradle.kts用include(":feature-home", ":core-net")声明模块,AGP 为每个模块生成独立任务子图,根assemble聚合全部。 - Flavor / Build Type:
productFlavors+buildTypes会在 Configuration 阶段展开组合(如paidDebug、freeRelease),任务数以乘积增长。 - 自定义任务接线:在
android { }闭包外用tasks.register("printApk") { dependsOn("assembleDebug") }挂到现有任务图。
常见误配、事故后果与排障
1. 误配:implementation 当 api 用,导致下游模块编译找不到类
- 现象:
Module B直接引用Module A通过implementation引入的三方类型,编译报Unresolved reference。 - 排障与修法:确认该类型是否应作为 A 的公开 API 边界;若是,把 A 的该依赖改为
api;若否,让 B 自己声明该依赖。
2. 事故:CI 未启用 Configuration Cache,全量配置拖慢流水线 40%+
- 误配原因:本地开了 Configuration Cache,CI 脚本没加
--configuration-cache,每次都重跑全部 build script。 - 后果:大仓多模块时配置时间随模块数线性增长,PR 构建排队变长。
- 排障与修法:CI 加
-Dorg.gradle.configuration-cache=true并保证所有自定义 task 兼容(任务需可序列化);先用--configuration-cache-problems=warn暴露不兼容 task。
3. 误配:compileOnly 引入运行期必需的依赖
- 后果:编译通过,运行期
NoClassDefFoundError。 - 修法:运行期需要的依赖用
implementation而非compileOnly。
相近概念对比
- Configuration Cache vs Build Cache:前者缓存"配置结果/任务图",后者缓存"任务输出";前者决定要不要重跑配置,后者决定任务是否 up-to-date。
- implementation vs api:影响消费方编译期可见性,不影响运行期 classpath(运行期传递依赖都在)。
- implementation vs compileOnly:
compileOnly连运行期都不带,是 NoClassDefFoundError 的高发区。 - Gradle Daemon vs Configuration Cache:Daemon 省 JVM 启动;Configuration Cache 省 build script 求值;两者可叠加。
对应实验
| 实验 | 说明 | 关键源码 |
|---|---|---|
| gradle-lifecycle-demo | 最小 Gradle 工程复演三阶段回调顺序:goodbye 已注册未执行证明「入选任务图 ≠ 被执行」;无 Gradle 环境可用 node syntax-check.mjs 静态自检(与文首「代码索引」一致) | settings.gradle.kts · build.gradle.kts |
| gradle-lifecycle-hook(Android host 版) | init script 零侵入注入版,观测既有大工程的三阶段输出 | 见该 lab README 内源码入口 |
文档内嵌脚本保留作为快速复习载体;可运行验证以 labs 为准。
复习检查题
Gradle 三阶段中,任务真正执行发生在哪一阶段?Configuration 阶段的产出是什么? 答:Execution 阶段执行任务;Configuration 阶段产出 Task Graph(任务执行图),也是 AGP 注册 Android 任务、按依赖排序的时机。
implementation与api的核心区别?为什么说它不会导致运行期 NoClassDefFoundError? 答:implementation不把传递依赖暴露给消费方编译期 classpath,api会暴露。运行期 Gradle 仍会把传递依赖放进消费方运行 classpath,所以二者都不会引起运行期缺失;运行期 NoClassDefFoundError 多来自compileOnly误用。Configuration Cache 与 Build Cache 分别解决什么问题? 答:Configuration Cache 缓存配置结果(任务图),避免重复执行 build script,解决"配置慢";Build Cache 缓存任务输出,跨构建/机器复用,解决"重复产出产物"。
速记
- 三阶段:Init(建 project)→ Config(跑脚本、建任务图)→ Exec(跑任务出产物)。
- assembleDebug:只是叶子,上游是编译→dex→打包的资源/代码任务。
- 三缓存:Configuration Cache(省配置)/ Build Cache(省产物)/ Daemon(省 JVM)。
- 依赖可见性:
api暴露、implementation隐藏、compileOnly运行期不带。