Gradle 构建基础:生命周期、Task 与 Plugin

聊 Gradle 优化之前,得先把它的骨架摸清楚。我刚开始接触 Android 构建时,总觉得 Gradle 就是个黑盒子——点一下运行,等半天出结果。后来踩了次坑,才知道不懂生命周期和 Task 机制,优化根本无从下手。

构建生命周期:三个阶段而已

Gradle 的构建过程,说白了就三个步骤:初始化 → 配置 → 执行。嗯,没错,和三段式电影一样,但这里没有反转。

阶段做什么我碰到的坑
初始化解析 settings.gradle,确定哪些子项目参与构建曾经把 settings.gradle 里 include 写错路径,导致整个项目找不到模块
配置执行每个项目的 build.gradle,生成 Task 的有向无环图早期我在配置阶段写了大量文件 IO 操作,结果配置时间比执行还长
执行按依赖顺序依次执行选中的 Task记得有一次执行时才发现 Task 依赖配了个死循环,Gradle 直接报错了

你可能会问:「为什么配置阶段那么慢?」其实很多新人会把耗时计算写在 build.gradle 的顶层,比如读取文件、解压 ZIP。我个人习惯把这些操作挪到 Task 的 doLast 里,只在执行时才跑。

切忌在配置阶段访问外部网络或执行耗时的 I/O 操作,否则每次执行 gradle tasks 都会卡半天。

Task——构建的最小单元

Task 就像乐高积木,每个 Task 干一件事。它有自己的动作(action)、输入、输出,还能声明依赖。我早期就吃过「不声明输入输出」的亏——改了源码,Gradle 还认为 Task 是最新的,根本不会重跑。

// 一个简单的自定义 Task
tasks.register('hello') {
    doLast {
        println 'Hello, Gradle!'
    }
}

// 带输入输出,实现增量编译
tasks.register('processData', Copy) {
    from 'src/data'
    into 'build/processed'
    // 输入是 src/data 目录,输出是 build/processed
    // 只要输入文件没变,Gradle 就跳过该 Task
}

这里有个关键点:增量构建。Task 的输入和输出就是 Gradle 判断「要不要重新执行」的依据。我建议你在写自定义 Task 时,一定显式声明 @Input 和 @Output,否则每次构建都是全量跑,还谈什么优化?

Gradle 的 up-to-date 检查依赖文件的修改时间戳和内容哈希。如果想强制跳过某个 Task,可以用 --rerun-tasks,但慎用。

Plugin——批量打包好的 Task 集合

Plugin 就是一群 Task 加上约定的组合。比如 java 插件提供了 compileJava、processResources、jar 等 Task;Android 插件提供了 mergeResources、dex、package 等等。你只需要 apply plugin: 'application',就能白嫖一堆现成的 Task。

但我见过很多团队把插件的应用顺序搞错。比如先应用自定义插件,再应用 java,结果自定义插件里依赖的 compileJava 还没注册,直接报错。我个人的做法是:插件应用写在 plugins { } 块里,让 Gradle 自动处理顺序。

// 推荐用法:plugins 块(Gradle 3.0+)
plugins {
    id 'java'
    id 'application'
}

// 老风格:apply plugin,不推荐
apply plugin: 'java'

它们如何协同工作?看这张图就清楚了

下面我把生命周期、Task 和 Plugin 的关系画成一张流程图。你看一眼就明白:Plugin 在配置阶段注册 Task,然后 Gradle 构建 Task 依赖图,最后在执行阶段依次跑。

Gradle 构建生命周期与 Task/Plugin 关系 初始化阶段 配置阶段 执行阶段 解析 build.gradle / 应用 Plugin 注册 Task & 构建依赖图 按依赖顺序执行 Task 增量检查 → 跳过/重跑 Plugin 在配置阶段注册 Task;Task 依赖图决定执行顺序;输入/输出决定是否增量执行。 用户编写 build.gradle 自定义 Plugin 官方 Plugin

这张图里的箭头方向你注意一下:初始化→配置→执行是主线;配置阶段内部会解析 build.gradle,应用 Plugin 并注册 Task;执行阶段则按照依赖图去跑 Task,同时利用输入输出判断能不能跳过。我曾经在一个项目里,由于某个 Task 没有声明输入,导致每次构建都重跑所有资源处理——光这一项就浪费了 2 分钟。后来加上 @InputFiles,增量构建生效,构建时间直接降到 40 秒。

避坑指南:我早期常犯的错误

  • 配置阶段调用 Runtime.exec():我曾经直接在 build.gradle 顶层写了一句 Runtime.runtime.exec("git rev-parse HEAD"),结果每次执行 gradle tasks 都触发一次 git 命令。后来改成在 Task 的 doFirst 里执行才好。
  • 忽略 Plugin 之间的冲突:同时应用 kotlin-android 和 kotlin-kapt 时,顺序错了会导致注解处理器找不到依赖。我的经验是:所有官方插件放在 plugins 块,自定义插件通过 apply from 单独引入,避免隐式依赖。
  • Task 依赖写死循环:比如 Task A 依赖 B,B 又依赖 A,Gradle 在配置阶段直接抛出 Circular dependency。遇到这种问题,赶紧检查 dependencies 块里有没有互相引用。

核心要点总结:

  • 生命周期三阶段:初始化(解析 settings)、配置(生成 Task 图)、执行(跑 Task)。
  • Task 必须声明输入/输出才能利用增量构建。
  • Plugin 是 Task 的集合,应用顺序影响注册时机。
  • 配置阶段要轻量,耗时操作移入 Task 动作。

嗯,有了这些基础知识,后面我们再谈具体的渲染瓶颈优化就顺手多了。记住:优化 Gradle 构建,核心就是管好你的 Task 图和输入输出声明。这章先讲到这儿,下节我们直接切入 Android 渲染场景下的构建痛点。