Skip to content

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)。

底层机制 ​

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. 阶段1-初始化:读取 settings,建立 project 集合
    2. 阶段2-配置:app(每个 project 依次回调)
    3. 阶段2结束:任务图就绪,共 N 个任务
    4. 任务逐个执行(如 > Task :app:compileDebugKotlin)
    5. 阶段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 / AABlibapp.so(AOT) / kernel(DJIT)bundle / chunkjar / war
增量机制up-to-date 检查 + Build Cache热重载(DJIT)HMR / 模块级重建插件增量
加速手段Configuration CacheDart 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 为准。

复习检查题 ​

  1. Gradle 三阶段中,任务真正执行发生在哪一阶段?Configuration 阶段的产出是什么? 答:Execution 阶段执行任务;Configuration 阶段产出 Task Graph(任务执行图),也是 AGP 注册 Android 任务、按依赖排序的时机。

  2. implementation 与 api 的核心区别?为什么说它不会导致运行期 NoClassDefFoundError? 答:implementation 不把传递依赖暴露给消费方编译期 classpath,api 会暴露。运行期 Gradle 仍会把传递依赖放进消费方运行 classpath,所以二者都不会引起运行期缺失;运行期 NoClassDefFoundError 多来自 compileOnly 误用。

  3. 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 运行期不带。

站点构建时间:2026/8/24 23:43:17