构建慢这件事,绝大多数团队都搞错了优化方向。
我见过太多项目组一抱怨 Gradle 慢,Leader 第一反应就是换 Mac、加内存、上固态,结果一万多块花出去,构建时间纹丝不动。然后一群人围着 Android Studio 里那个进度条大眼瞪小眼,谁也不知道瓶颈到底卡在哪个阶段。
先把结论放这儿:Gradle 构建慢,90% 的情况不是硬件问题,而是你没有把 Java 运行时、Gradle 配置策略和 Android 任务链这三层的关系理顺。这三层就像一辆车的发动机、变速箱和轮胎,任何一个环节不匹配,踩油门都白搭。
这篇文章我会把我压榨 Gradle 构建性能的完整思路拆开讲,从 JVM 底层参数到 Android 物理打包链路,每一步都给出可复现的配置和验证方法。适合被构建速度折磨了几个月、试过网上零散技巧但始终没系统解决的人,也适合想把自己项目的构建配置从“能用”提升到“能打”的 Android 开发者。
1. 构建慢不是因为电脑差:先把瓶颈量化出来
我接手过的每个“慢项目”,几乎都有一个共同特征:没人跑过构建分析。都是在凭感觉猜——有人说依赖太多,有人说资源文件太大,有人说该换电脑了。
其实 Gradle 自带一套相当完整的性能分析工具,只是大多数人装完 Android Studio 就再也没碰过这些命令。我强烈建议你做优化的第一步不是改任何配置,而是先量化。
1.1 用 --profile 和 --scan 给构建做一次全身体检
先跑一条最基础的命令,给当前项目生成一份构建画像:
bash复制./gradlew assembleDebug --profile --rerun-tasks
--profile 会在 build/reports/profile/ 目录下生成一份 HTML 报告,里面按阶段把构建时间拆得明明白白:初始化阶段、配置阶段、任务执行阶段,每个任务单独花了多少毫秒,一目了然。
--rerun-tasks 是我故意加的,因为如果不加,增量构建会让很多任务直接 UP-TO-DATE 跳过,你看到的只是缓存命中的假象,根本测不出真实耗时。
如果项目配置了 Build Scan(Gradle 7.x 以上用 --scan 参数),还能得到一份在线报告,包含依赖解析耗时、缓存命中率、GC 时间等更细粒度的数据。
这份报告拿到手,先看三个关键指标:
- 配置阶段耗时占比:如果配置阶段占了总时间 30% 以上,说明项目里大量任务在配置阶段被初始化了,这是后面第三个章节要解决的核心问题。
- 单任务耗时 Top 榜:排在前面的往往是
processDebugResources、dexBuilderDebug、mergeExtDexDebug之类的 Android 任务,或者是某些自定义的 Transform 任务。 - GC 时间:如果 GC 时间占了执行阶段的 15% 以上,说明 JVM 堆内存或垃圾回收器选型有问题。
1.2 一个小项目拖成“大慢车”的真实案例
有一个外包项目,业务代码不到 2 万行,但冷构建要跑 4 分 20 秒。当时团队给我的解释是“资源文件太大了”,我跑了一次 --profile 发现,光配置阶段就花了 47 秒。
点进去看详情,才发现在 build.gradle 的顶层直接执行了 new File(...).eachFile { ... } 去做资源目录扫描,还有一个自定义插件在配置阶段联网请求了一个接口来判断版本号。这两个操作本身就够慢的,而且每次构建都会执行,不管你改没改代码。
这就是典型的配置阶段污染——把本应在执行阶段做的事放到了配置阶段去做,导致每次构建的固定开销被无限放大。
在动任何 Gradle 配置之前,先找到真实的瓶颈在哪,这一步花 10 分钟,后面能省 10 个小时。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Java 运行时压榨:守护进程、内存与编译策略的正确姿势
第一层要优化的是 Gradle 所依赖的 Java 运行时。这一层最容易被忽视,因为 Android 开发者平时写代码不直接接触 JVM 参数,但这些参数恰恰决定了构建的“底力”。
2.1 守护进程不是越大越好:org.gradle.jvmargs 的正确打开方式
网上流传的配置经常是抄来抄去,什么 -Xmx8g、-Xmx16g 都有人敢填。但 JVM 堆内存不是越大越好——堆太大导致 GC 停顿时间变长,堆太小导致频繁 Full GC,两者都拖慢构建。
我的推荐起手式:
properties复制org.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
6g 这个值怎么来的?不是拍脑袋,是观察出来的。我用 JVisualVM 挂过构建进程,大多数中大型 Android 项目的构建峰值堆占用在 4g 左右,留出 50% 余量到 6g 就足够了。
真正影响 GC 停顿的,往往是垃圾回收器的选择。Gradle 从 7.x 开始默认启用了 G1GC,但我实测下来,在构建这种“大量短生命周期对象 + 偶尔大对象分配”的场景里,ParallelGC 在部分 JDK 版本上表现反而更稳。
不过这里我不建议你盲目换,因为不同 JDK 版本对 GC 的调校差异很大。如果你用的是 JDK 17,G1GC 已经优化得很好了;如果还在 JDK 8 或 11,可以试试加这一行做 A/B 对比:
properties复制org.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=1g -XX:+UseParallelGC
对比方法还是一样,--profile 跑两遍,看执行阶段耗时和 GC 耗时。
2.2 JDK 版本选型:这一步错了后面全是坑
很多人的 Gradle 构建慢,根本原因是 JDK 版本和 Gradle 版本不匹配,导致 Gradle 跑在兼容模式下,很多新特性白白失效。
目前最稳定的组合:
| Gradle 版本 | 最低 JDK | 推荐 JDK | 对应 AGP |
|---|---|---|---|
| 7.x | JDK 8 | JDK 11 | AGP 7.x |
| 8.x | JDK 8 | JDK 17 | AGP 8.x |
| 8.5+ | JDK 8 | JDK 17 | AGP 8.x+ |
如果你还在用 JDK 8 跑 Gradle 8.x,虽然能跑,但 Gradle 8 基于 JDK 17 编译的核心类库在 8 上面只能走解释执行路径,性能打折扣是必然的。
检查当前项目用的 JDK 版本:
bash复制./gradlew --version
如果输出里显示的 JVM 版本和你系统里 java -version 不一致,说明 Android Studio 用的是它自带的 JBR(JetBrains Runtime),而不是系统 JDK。这里记住一个原则:Gradle 守护进程用什么 Java 跑,以 gradle --version 输出为准。
2.3 开启编译并行和按需配置的前提
Gradle 有两个老牌开关,几乎每篇优化文章都会提,但很少有人讲清楚它们的副作用。
properties复制org.gradle.parallel=true
org.gradle.caching=true
parallel 开启后,多个 module 的任务可以在不同线程并行执行。这个开关对多模块项目的收益非常明显,但它会增加 CPU 压力,如果你用的是 4 核以下的机器,开了反而可能更慢。
caching 是构建缓存,它会将任务输出缓存到本地甚至远程,让相同输入的任务直接跳过执行。但它的前提是——所有任务都必须正确声明输入和输出。如果你的项目里有自定义 Task 没声明 @Input 和 @Output,打开缓存会导致缓存命中错乱,出现“改了代码但没生效”的诡异问题。
所以我的建议是:先保证项目里所有自定义任务都规范声明了输入输出,再开 caching,否则宁可不开。
Java 层优化做完,你应该能感受到的是构建峰值 CPU 利用率上来了,构建时间能压缩 15%-25%。
3. Gradle 配置阶段的减负:光改一堆开关治标不治本
前两年有人做过统计,一个典型的 Android 项目,配置阶段占了整个构建时间的 25% 到 40%,而且这个比例会随着你引入的插件和模块数量急剧恶化。
为什么?因为 Gradle 的执行模型分两个阶段:配置阶段和执行阶段。配置阶段不管你是不是只改了一个 Java 文件,它都会把所有模块的 build.gradle 全部执行一遍,创建所有 Task 实例、解析所有依赖坐标、执行所有顶层代码。
3.1 配置阶段的最大杀手:在顶层瞎写代码
我在第一节提到的那个 47 秒配置阶段的案例,就是这个问题的极端体现。更常见的版本是:
- 在
build.gradle顶层做文件遍历、递归查找 - 在配置阶段调用
exec执行 Shell 命令 - 在插件里用
project.afterEvaluate疯狂注册任务,把原本可以惰性创建的 Task 全部提前实例化
这些都是反模式。正确做法是用 Gradle 的惰性配置 API。举个例子,如果你想创建一个自定义任务:
groovy复制tasks.register("myTask") {
// 这里是配置逻辑,只有任务要执行时才触发
}.configure {
// 这里是任务执行逻辑
}
register 和 create 的区别就在这——register 只是注册了一个 Task Provider,不会立即创建实例,等你真正需要这个 Task 时才实例化。项目里所有任务都应该用 register 而不是 create。
3.2 依赖解析优化:锁定版本,别用动态版本
另一个配置阶段的隐形杀手是依赖解析。很多人喜欢写:
groovy复制implementation 'com.squareup.okhttp3:okhttp:4.+'
这个 4.+ 是动态版本号,Gradle 每次构建都要去远程仓库查最新版本,这会显著增加依赖解析的时间和不确定性。更坑的是,它还会破坏构建缓存——因为依赖坐标不确定,缓存 key 也会跟着抖动。
我的做法是全局锁定版本,用 Gradle 自带的功能生成一份依赖锁文件:
bash复制./gradlew dependencies --write-locks
生成 gradle.lockfile 后,所有依赖版本被锁定,不仅构建可复现性大大提高,依赖解析速度也能明显提升。
3.3 Configuration Cache:能开就开,但不能无脑开
Gradle 7.4 之后,配置缓存(Configuration Cache)算是革命性的功能。它把配置阶段的结果序列化缓存下来,下次构建直接跳过配置阶段,效果可以用“立竿见影”形容。
code复制org.gradle.configuration-cache=true
我见过配置阶段 50 秒的项目,开启后配置时间直接降到 3 秒以内。
但它有两个前提条件:
- 项目里所有插件和构建脚本必须兼容配置缓存,否则会报一堆“问题:访问了不可以访问的项目属性”之类的错误。
- 自定义 Task 必须避免在配置时访问外部状态,比如环境变量、随机值、当前时间,这些都会破坏缓存的有效性。
所以我的策略是:先开在一台机器上试跑一遍完整构建,收集所有兼容性报错,逐个修复后再全量推开。如果你们的项目有一堆老插件改不动,也不要硬扛——可以先用 Gralde 8 带的 --configuration-cache-problems=warn 模式过渡,至少能拿到大部分缓存收益。
配置阶段优化是投入产出比最高的一层,往往能把构建时间直接砍掉 50% 以上,但也是最需要测试的一层,改完一定要全量回归一遍。
4. Android 任务链的物理压榨:从资源合并到 APK 产物的每毫秒
前两层优化搞定之后,构建时间的大头会转移到 Android 本身的构建任务链上,也就是 processDebugResources、mergeDex、packageDebug 这些 AGP 任务。这一层我们已经进入 Android 构建的“物理层面”,每一步都是硬功夫。
4.1 别再让 debug 包做 release 的事:按构建变体定制任务链
这是我在无数项目里看到的最常见浪费——debug 构建执行了根本不需要的 proguard 混淆、lintVitalRelease、shrinkResources,而这些操作对本地调试毫无意义。
正确做法是用 Gradle 的 buildTypes + productFlavors 把 debug 链路和 release 链路彻底分离开:
groovy复制android {
buildTypes {
debug {
// debug 不开启混淆和资源压缩
minifyEnabled false
shrinkResources false
}
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
有一说一,minifyEnabled false 是 AGP 默认值,但我见过太多项目在 debug 变体下手动开了混淆去“模拟 release 问题”,结果就是每次本地构建都要多花一两分钟的混淆时间。有那功夫不如直接跑 release 构建。
4.2 资源处理的压榨:resConfigs 与向量图标的配合
资源处理是 Android 构建链路的重头戏。尤其是 processDebugResources,它要解析你的所有资源文件、生成 R 类、做资源索引,复杂度极高。
一个经常被忽略的优化点是,项目里引入的第三方库(比如 appcompat、material)会自带几十种语言的翻译资源,AGP 默认会把它们全部处理进 APK。用 resConfigs 可以只保留你需要的语言:
groovy复制android {
defaultConfig {
resConfigs "zh", "en"
}
}
这不仅让 APK 体积变小,还能让资源处理的耗时直接减少一大截。
另一个优化是能用矢量图就别用 PNG。因为 AGP 处理矢量图(VectorDrawable)的速度远快于位图压缩,而且自带密度适配。为了兼容老版本,可以用 vectorDrawables.useSupportLibrary = true。
4.3 依赖与 dex 的物理链路:需要接受的一个事实
dex 编译(D8/D8)是 CPU 密集型的硬活,它要把你的全部 class 文件转换成 dex 字节码。这一步的耗时直接和你依赖的库数量成正比。
所以从构建性能的角度来看,每次引入一个新的第三方库,都是在给构建速度加负担。这不是说不能引库,而是要有意识:
- 两个功能重叠的库,只留一个
- 能本地写几十行代码解决的小功能,别急着引库
- 用
implementation而不是api暴露依赖,减少传递依赖的 dex 转化压力
到了这一层,--profile 报告里大头的就是这些任务的耗时。物理压榨没有银弹,只能从依赖瘦身、资源精简、任务跳过这些硬指标上一刀一刀地砍。
5. 依赖获取的第三种解法:镜像、离线包与本地仓库的配合
从 Java 代码到 Gradle 配置都调优完之后,还有一个经常让人崩溃的环节:依赖下载。尤其是新环境第一次构建,光是下载 Gradle 发行版和依赖包,就能把人的耐心彻底消磨光。
这个部分我结合我自己的实操,说三个立竿见影的配置方案。
5.1 换镜像不是可耻的事:仓库与发行版的加速
先分清两件事:Gradle 发行版的下载(那个几百 MB 的 zip)和依赖包的下载(那些几十 MB 的 jar/aar)走的是两条路。
发行版下载地址在 gradle/wrapper/gradle-wrapper.properties 里:
properties复制distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
这个地址在国内访问极不稳定,建议换成你所在区域云厂商的 Gradle 镜像地址,因为是官方 Gradle 发行版的镜像分发,内容完全一致,但速度快一个量级。
依赖包的仓库地址则在 settings.gradle 或根 build.gradle 的 repositories 里配置:
groovy复制pluginManagement {
repositories {
maven { url 'https://mirrors.cloud.tencent.com/nexus/repository/maven-public/' }
maven { url 'https://maven.aliyun.com/repository/public' }
maven { url 'https://maven.aliyun.com/repository/google' }
maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
google()
mavenCentral()
}
}
这里我贴的是腾讯和阿里云的公共 Maven 镜像配置。它们是云厂商提供的公网依赖加速服务,把 Google 的 Maven 仓库和 Maven Central 的内容做了同步分发。配置顺序上,把国内镜像放在最前面,Gradle 解析依赖时按顺序访问,命中即停。
5.2 离线包方案:新同事入职构建从半小时到 3 分钟的实践
如果你的团队经常有人换电脑、新同事入职,或者 CI 机器的配置经常重置,那一定要把离线包方案用起来。
新环境构建慢的核心原因,是 Gradle 的依赖缓存(默认在 ~/.gradle/caches/)是空的,所有依赖要从头下载。与其让每个人各自下载一遍,不如在一台干净的机器上把依赖拉全,然后直接拷贝缓存目录。
具体操作:
- 在一台已跑通构建的机器上,执行一次全量构建,确保依赖缓存完整
- 压缩
~/.gradle/目录(去掉 daemon 和 wrapper 里的旧版本) - 新环境解压到同样的路径
这套方案我实测下来,一个新环境从“下载 Gradle + 拉取依赖”的半小时以上,压缩到了 3 分钟以内。
如果项目更规范一点,可以把常用的 Gradle 发行版 zip 放到内网共享盘或对象存储上,然后修改 gradle-wrapper.properties 的 distributionUrl 指向内网地址。这样每次 ./gradlew 拉取发行版也不会走外网。
5.3 让 Gradle 生成本地 Maven 仓库的另类用途
Gradle 有一项不太起眼但很实用的能力:maven-publish 插件可以把模块打成本地 Maven 仓库。很多人只在发布 SDK 时用它,其实它也是优化多项目依赖解析的一把好手。
我有一次优化一个超大型多模块项目,发现各个模块之间虽然是项目依赖,但某个公共模块被多个模块反复编译,导致任务链非常长。后来我把这个公共模块发布到了本地的 mavenLocal() 仓库,其他模块直接引用本地坐标,构建时间立刻降了一截——当然这个方案不适合日常迭代,因为公共模块的改动不能实时同步,但它证明了“依赖解析路径再怎么优化,都不如直接减少解析次数”的理念。
依赖层优化的核心思路是:让每一次依赖解析都命中缓存,让每一次缓存都尽量复用,从机制上减少网络 IO 和磁盘 IO 的负担。
6. 报错即优化线索:几个高频 Gradle 诊断词背后的真相
配置写得再好,实际构建中还是会冒出一堆问题。很多人遇到报错第一反应是“搜一下怎么解决”,但其实很多报错本身就藏着你项目配置的病根。
6.1 “Deprecated Gradle features were used in this build”
这条警告信息在网上被问了无数遍,但真正看懂它的人不多。它的完整意思是你项目里的某个插件或构建脚本使用了旧版 Gradle API,而这个 API 在新版本中已经被标记为废弃。
问题在于,Gradle 对废弃 API 不会直接报错,而是切换到兼容模式运行。兼容模式意味着它要走额外的适配层,这对构建性能同样是负担。
处理方式不是去搜“如何关闭这条警告”,而是找到是哪段代码用了废弃 API:
bash复制./gradlew build --warning-mode=all
这个命令会把所有废弃 API 的使用位置详细列出来。绝大多数情况是某个第三方插件适配不到位,可以先查插件有没有新版本;如果是自己写的构建脚本,按提示修改 API 调用即可。
6.2 “You are applying Flutter's main Gradle plugin imperatively”
这个报错对 Flutter 开发者来说是老朋友了,它说的是 Flutter 的 Gradle 插件用的是命令式 apply 方式接入,而新的 Gradle 插件机制推荐用 plugins {} 声明式接入。
命令式 apply 的问题在于,Gradle 无法在插件应用之前对插件做依赖管理和版本解析优化,这在某些场景下会影响配置阶段的性能。
绕过这个问题很简单,在 settings.gradle 里改用 plugins DSL:
groovy复制plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
}
6.3 每次新建项目都要下载 Gradle:90% 的人是同一个原因
网上最常被问的问题之一就是“为什么 Android Studio 每次新建项目都要下载一次 Gradle”。大概率是你的 gradle-wrapper.properties 里指定的 Gradle 版本和你本地缓存里的不一致。
Android Studio 新建项目时会用模板自带的 Gradle 版本,而模板版本往往不是最新但也不是你常用的,所以每次都要去下载。解决办法:
- 找到你本地已经缓存好的 Gradle 版本:
ls ~/.gradle/wrapper/dists/ - 把项目的
gradle-wrapper.properties里的distributionUrl改成这个版本 - 离线包方案同理,统一团队所有人使用同一个 Gradle 版本
6.4 Java 环境变量配置错误的隐性影响
最后一个隐藏很深的坑:JAVA_HOME 指向不明确。很多机器上装了好几个 JDK,Android Studio 自带的 JBR、系统 Java、还有 SDK 里的 JDK。环境变量配置一旦混乱,Gradle 可能每次都在用不同版本的 Java 启动守护进程,导致守护进程不断重启,缓存全部作废。
检查方法:
bash复制./gradlew --status
如果输出里 PID 不断变化,或者 Java 版本一会儿 11 一会儿 17,说明守护进程经常被杀掉重启。这时你需要统一 JAVA_HOME 和 PATH 的指向,让 Gradle 始终使用同一个 JDK。
每个报错都是一次免费的构建性能审计机会——它精准地告诉你是哪个插件、哪段脚本、哪个配置拖了后腿。抓住它们,优化就成功了一半。
7. 最后:我的个人压箱底建议
如果你看完这篇文章只想记住三件事,那我希望是以下几点。
第一,优化前必须跑一次 --profile。不要凭感觉猜瓶颈,数据会告诉你真实答案。我见过太多人在资源压缩上花了一整天,结果瓶颈根本不在那里。
第二,优化永远从配置阶段开始。配置阶段是每次构建的固定开销,优化一次,所有构建永久受益。配置阶段减掉的每一秒,都比执行阶段减掉的十秒值钱。
第三,构建性能是团队工程,不是个人英雄主义。你花时间把 gradle-wrapper.properties 统一了,把依赖锁定了,把缓存目录分享到内网了,这些影响的是每个开发者和 CI 的每一次构建。真正高效的团队,会把构建时长纳入 CI 的监控指标,每次合并代码后自动对比,防止性能回退。
还有一个经验想分享:不要总想着一步到位。把上面的优化项分成几个小批次逐步上线,每次只改一个配置,跑一遍完整构建,记录耗时,再改下一个。如果直接一次性全改完,出了问题你根本不知道是哪个配置引起的,到时候回退都不知道该回退哪个,这才是最耗时间的。
构建优化这条路没有终点,但每一步的收益都是实打实的。希望你读完这篇能少走点弯路,尽早把等构建的时间省下来,花在真正值得的事情上。
