2. 构建生命周期:初始化、配置、执行三个阶段;生命周期钩子函数详解

Gradle 的构建过程,说白了就是一场精心编排的三幕剧。很多人用 Gradle 好几年,却从来没关心过构建到底是怎么一步步走下来的。结果出了问题,比如某个配置明明写了却不起作用、Task 执行顺序乱套,只能靠瞎猜去排查。

我个人习惯把 Gradle 构建的生命周期拆成三个阶段来理解:初始化(Initialization)、配置(Configuration)、执行(Execution)。记住这个骨架,你就能看懂大部分构建问题了。

核心观点:构建生命周期不是 Gradle 凭空拍出来的,它是为了解决多项目构建和延迟配置而设计的。理解它,你才能真正驾驭 Gradle。
Gradle 构建生命周期与钩子函数 初始化阶段 ● 解析 settings.gradle ● 确定参与构建的 Project ● 创建 Gradle 实例 配置阶段 ● 解析每个 Project 的 build.gradle ● 创建并配置所有 Task ● 依赖关系图构建 执行阶段 ● 按依赖顺序执行 Task ● 构建产物生成 ● 输出结果 & 清理 常用钩子函数 gradle.settingsEvaluated gradle.projectsLoaded project.beforeEvaluate project.afterEvaluate gradle.taskGraph.whenReady gradle.buildFinished 💡 钩子函数分布在三个阶段中:settingsEvaluated/projectsLoaded 在初始化阶段触发; beforeEvaluate/afterEvaluate/whenReady 在配置阶段;buildFinished 在执行阶段结束后触发。

2.1 初始化阶段:谁该参与这次构建?

当你敲下 gradlew build 的时候,Gradle 要做的第一件事不是直接去编译代码,而是先搞清楚——这次构建涉及哪些项目? 这就是初始化阶段干的事。

  • 查找并解析 settings.gradle:Gradle 从当前目录向上寻找 settings.gradle。如果找不到,就默认单项目构建;找到了,就根据里面的 include 声明确定要引入的子项目。
  • 创建 Gradle 实例和 Project 对象:每个被包含的项目都会生成一个 Project 对象,但此时它们还没加载 build.gradle 中的配置。
  • 应用初始化脚本:位于 ~/.gradle/init.d/ 或项目根目录下的 init.gradle 会在这个阶段执行。
我个人的经验:如果发现某些全局配置(比如自定义插件仓库)不生效,检查一下是不是把代码写到了 build.gradle 里,而插件初始化需要在 init.gradle 中完成。初始化阶段只有那些被显式 include 的项目才会出现,千万别指望在 settings 里用 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 并发运行。
  • 构建缓存:缓存命中时直接从缓存获取结果,连执行都不需要。
我曾经接手过一个项目,构建时间从 30 分钟降到 5 分钟,方法就是定位到配置阶段里有一段解析 XML 的逻辑——它居然在 build.gradle 的顶层写了个 new XmlParser().parse(...)。每次配置阶段都会解析,而项目有 20 多个模块,等于重复解析了 20 多次。后来我把它挪到 Task 的 doLast 里,配置阶段瞬间变快。

2.4 生命周期钩子函数详解

Gradle 在三个阶段的关键节点暴露了一些钩子(Hook),让我们有机会嵌入自定义代码。别小看这些钩子,很多高级优化技巧(比如动态修改 Task、条件化配置)都依赖它们。

钩子名称触发时机所属阶段常用场景
settingsEvaluatedsettings.gradle 解析完成初始化阶段读取 settings 中的属性,动态添加项目
projectsLoaded所有 Project 对象已创建初始化 → 配置过渡对所有项目统一配置一些默认行为
beforeEvaluate每个 Project 开始执行 build.gradle 前配置阶段为项目注入额外的属性或依赖
afterEvaluate每个 Project 的 build.gradle 执行完毕配置阶段修改已配置的 Task,或者添加额外条件
taskGraph.whenReadyTask 依赖图构建完成,但尚未执行配置阶段末尾检查最终依赖图,调整 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 之前用条件判断。
说白了,生命周期钩子就是编程里的观察者模式。你不需要去猜 Gradle 什么时候做了什么,只要注册好对应的监听器,它自动会回调。掌握好这三个阶段和几个关键钩子,你就能在构建的任意时间点插入逻辑,实现自动化、自定义和性能优化。