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 里,只在执行时才跑。
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,否则每次构建都是全量跑,还谈什么优化?
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 依赖图,最后在执行阶段依次跑。
这张图里的箭头方向你注意一下:初始化→配置→执行是主线;配置阶段内部会解析 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 渲染场景下的构建痛点。