7、生命周期与钩子:Gradle构建阶段、Project与Settings回调、TaskGraph监听、buildFinished与buildStarted

Gradle 的构建过程,说白了就是一场精心编排的舞台剧。每个演员(Task)什么时候上场,什么时候退场,都有严格的剧本。这个剧本,就是 Gradle 的生命周期。

我刚开始接触 Gradle 时,总觉得它像个黑盒子。点一下运行,哗啦啦一堆日志,然后就出 APK 了。至于中间发生了什么,完全没概念。直到有一次,我在自定义插件里写了个耗时操作,结果构建卡死,我才意识到——不了解生命周期,你连问题出在哪都找不到

这一章,我们就来拆解 Gradle 的构建阶段,以及那些关键的钩子回调。掌握了这些,你就能在构建的任意时刻插入自己的逻辑。

7.1 三大构建阶段

Gradle 的构建过程分为三个阶段:初始化(Initialization)配置(Configuration)执行(Execution)。嗯,简称 ICE,好记。

阶段 做什么 关键对象
初始化 解析 settings.gradle,确定哪些项目参与构建 Settings
配置 解析每个项目的 build.gradle,创建 Task 对象 Project
执行 按依赖顺序运行选中的 Task Task

你想想看,这三个阶段是串行的。配置阶段没跑完,执行阶段绝对不会开始。我在项目中遇到过有人想在配置阶段打印 Task 的执行结果——那当然打印不出来,因为 Task 还没跑呢。

7.2 初始化阶段与 Settings 回调

初始化阶段,Gradle 会先找 settings.gradle(或 settings.gradle.kts)。这个文件决定了你的项目是单模块还是多模块。

我个人习惯在 settings.gradle.kts 里做两件事:

  • 声明模块路径
  • 配置插件管理仓库

但很多人不知道,settings.gradle 里也能挂回调。比如 gradle.settingsEvaluatedgradle.projectsLoaded

// settings.gradle.kts
gradle.settingsEvaluated {
    println("Settings 已解析完成")
}

gradle.projectsLoaded {
    println("所有 Project 对象已创建")
}

这两个回调有什么用?举个例子。我曾经需要根据 settings 里的某个属性,动态决定是否加载某个模块。在 settingsEvaluated 里判断,就能提前做决策,避免无效加载。

小提示:初始化阶段的回调,执行时机非常早。如果你要在构建一开始就打印一些诊断信息,这里是最佳位置。

7.3 配置阶段与 Project 回调

配置阶段是 Gradle 最“热闹”的阶段。每个模块的 build.gradle 都会被解析,所有的 Task 对象被创建,依赖关系被建立。

这个阶段有几个重要的钩子:

  • project.beforeEvaluate:在 Project 被配置之前触发
  • project.afterEvaluate:在 Project 配置完成后触发
  • gradle.projectsEvaluated:所有 Project 都配置完成后触发

看个例子:

// 在 app/build.gradle.kts 中
project.beforeEvaluate {
    println("${project.name} 即将开始配置")
}

project.afterEvaluate {
    println("${project.name} 配置完成")
}

gradle.projectsEvaluated {
    println("所有项目配置完毕")
}

这里有个坑,我必须要说。 beforeEvaluate 这个回调,如果你写在 build.gradle 文件里,它可能不会执行。为什么?因为当 Gradle 开始解析这个文件时,配置阶段已经开始了,beforeEvaluate 的时机已经错过了。

避坑指南:我曾经在 build.gradle 里写 beforeEvaluate,调试了半天就是不触发。后来才发现,这个回调只能在插件或 settings.gradle 里注册才有效。切记!

7.4 TaskGraph 监听

配置阶段结束后,Gradle 会生成一个完整的 Task 依赖图(TaskGraph)。这个图决定了 Task 的执行顺序。

TaskGraph 提供了两个关键回调:

  • whenReady:Task 图构建完成,但还没开始执行
  • beforeTask / afterTask:每个 Task 执行前后触发
gradle.taskGraph.whenReady { graph ->
    println("Task 图已就绪,共 ${graph.allTasks.size} 个 Task")
}

gradle.taskGraph.beforeTask { task ->
    println("即将执行: ${task.name}")
}

gradle.taskGraph.afterTask { task ->
    println("执行完毕: ${task.name}")
}

我个人觉得 whenReady 特别有用。比如你想在构建开始前检查一下,有没有某个 Task 被包含进来。如果没有,就提前报错,而不是等到执行到一半才崩溃。

举个例子:

gradle.taskGraph.whenReady {
    if (hasTask(":app:assembleRelease")) {
        println("Release 构建已确认")
    } else {
        throw GradleException("缺少 assembleRelease Task,请检查配置")
    }
}

嗯,这种“提前校验”的思路,能帮你省下不少调试时间。

7.5 buildFinished 与 buildStarted

这两个回调,一个在构建开始前,一个在构建结束后。它们属于 Gradle 生命周期里最外层的钩子。

  • gradle.buildStarted:构建开始前触发(注意:这个回调在 settings 或 init script 中注册才有效)
  • gradle.buildFinished:构建结束后触发,无论成功还是失败
// 在 init.gradle 或 settings.gradle 中
gradle.buildStarted {
    println("构建开始于 ${System.currentTimeMillis()}")
}

gradle.buildFinished { result ->
    println("构建结束,状态: ${result.failure ? "失败" : "成功"}")
    if (result.failure != null) {
        println("异常信息: ${result.failure.message}")
    }
}

我曾经用 buildFinished 做过一个自动上报构建耗时的小工具。每次构建完,把耗时、成功/失败状态、模块数量等信息,通过 HTTP 发到团队的后台。这样就能统计出哪个模块构建最慢,哪个开发者经常构建失败。

核心要点:buildStarted 的注册时机非常苛刻,必须在初始化阶段之前。所以一般放在 init.gradle 或 gradle 启动参数里。而 buildFinished 则灵活得多,几乎任何地方都能注册。

7.6 一张图看懂生命周期

说了这么多,不如一张图来得直观。下面是我画的 Gradle 构建生命周期流程图,包含了各个阶段和关键回调的位置。

Gradle 构建生命周期流程图 初始化阶段 解析 settings.gradle 配置阶段 解析 build.gradle 执行阶段 运行 Task 关键回调位置 buildStarted settingsEvaluated beforeEvaluate afterEvaluate projectsEvaluated whenReady beforeTask afterTask buildFinished (无论成功或失败) 三个阶段串行执行,回调在对应阶段触发

7.7 实战:用生命周期钩子做构建耗时统计

光说不练假把式。我们结合前面学的,写一个简单的构建耗时统计插件。

// BuildTimerPlugin.kt
class BuildTimerPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        val startTime = System.currentTimeMillis()

        project.gradle.buildStarted {
            println("构建开始")
        }

        project.gradle.taskGraph.whenReady {
            println("Task 图就绪,准备执行")
        }

        project.gradle.buildFinished { result ->
            val duration = System.currentTimeMillis() - startTime
            println("构建耗时: ${duration}ms")
            if (result.failure != null) {
                println("构建失败: ${result.failure.message}")
            }
        }
    }
}

这个插件虽然简单,但很实用。我在团队里推广过,大家反馈说能直观看到每次构建花了多久,对优化构建速度很有帮助。

小技巧:如果你只想在某个模块里监听,可以在 afterEvaluate 里注册 buildFinished。这样就不会影响其他模块。

好了,这一章的内容就到这里。生命周期这东西,刚开始觉得抽象,用多了就会发现——它其实就是 Gradle 给你留的一扇窗。透过这扇窗,你可以在构建的任意时刻做你想做的事。


公众号:蓝海资料掘金营,微信deep3321