2. 构建耗时的常见痛点归纳总结

说实话,大多数团队碰到 Gradle 构建慢的时候,第一反应就是「加内存」、「切 KSP」或者「换 SSD」。但这些操作很多时候只是安慰剂。你得先搞清楚,到底慢在哪一步?

我个人的经验是——网络瀑布图是诊断构建瓶颈的一把手术刀。但在用它之前,咱们得先把常见的痛点扫一遍。这样你拿着瀑布图去看,才能一眼认出「哦,原来又是这个老毛病」。

下面我把团队里踩过的坑,以及社区里高频出现的问题,归纳成六大类。

2.1 依赖解析 —— 悄无声息的吞时怪

依赖解析,说白了就是 Gradle 去拉 jar 包、aar 的过程。这个过程最玄乎:你明明没改任何代码,第二次构建却也可能等上几十秒。

为什么?因为 Gradle 默认的依赖缓存策略是「动态版本 + 快照」的过时检查。哪怕你用了 1.2.3 这样的固定版本,如果父 pom 里引用了 2.x 这种动态范围,Gradle 还是得联网去检查一遍。

避坑指南:我曾经遇到一个项目,每次构建光依赖解析就占 40% 的总时间。瀑布图上一排密密的浅蓝条,全是 HTTP 请求。后来发现是某个库的 transitive dependency 配了 2.+ —— 气得我直接把它 exclude 了。

依赖解析常见症状:

  • 每次构建都有大量细碎的 GET 请求(十几个甚至几十个)
  • 第一次构建巨慢,后续缓存后稍快(但仍会检查部分动态版本)
  • 内网仓库的 DNS 解析慢(瀑布图上会出现一段空白等待)

2.2 任务配置不当 —— 串行化与冗余计算

Gradle 构建的本质是 Task 的执行。很多项目的 Task 图看起来漂亮,但执行起来「各跑各的」,没有充分利用并行能力。

我最常见的问题就两个:

  1. 没有正确声明 Task 输入/输出 —— 导致增量编译失效,每次全量跑。
  2. 自定义 Task 里写了大量文件扫描或 IO 操作 —— 比如每个模块编译前都要扫描一次整个工程目录。
小技巧:用 ./gradlew build --scan 查看 Task 的执行时间线。如果发现很多 Task 是串行执行的,并且没有依赖关系,那就是 parallel 没开。你可以在 gradle.properties 里加一句:org.gradle.parallel=true。

2.3 增量编译失效 —— 白跑一遍的痛

增量编译是 Gradle 最核心的优化机制。但它很容易被破坏。最常见的「杀手」有:

  • Java 注解处理器输出路径全局化(比如统一写到 build/generated,导致每次都会触发重新生成)
  • Kotlin 的 kapt —— 它的增量支持一直不太稳定,尤其在多模块场景下
  • 使用了 project.fileTree() 且包含通配符 —— Gradle 无法精确判断哪些文件变了,索性全量跑

我以前在一个 30 个模块的项目里,发现某个模块的 Java 编译时间从 2 秒变成了 15 秒。查了半天——原来有个同事在 build.gradle 里写了一句:inputs.dir fileTree('src/main'),导致每次构建都认为所有源文件都变更了。

2.4 资源处理瓶颈 —— aapt2 和 shrink 的「雷」

Android 工程的资源处理(mergeResources + processResources + aapt2)是构建中最重的任务之一。我见过最夸张的一个案例,光 mergeDebugResources 就跑了 50 秒。

常见的元凶有:

症状可能原因快速定位方法
mergeResources 耗时异常资源文件太多(超过 3 万个文件)看瀑布图上的条宽,对比其他模块
processDebugResources 变慢aapt2 版本不兼容,开启了 crunchPng检查 aapt2 路径,尝试关闭 PNG 压缩
shrinkResources 拖后腿引用了大屏适配的 baseline 资源未配置瀑布图上会有明显的长条,且后续任务空闲
个人建议:如果你的项目资源文件超过 1 万个,考虑使用 android.enableResourceOptimizations=true 并配合 resourcePrefix 规范。

2.5 构建缓存未命中 —— 看似快实则没用到

构建缓存(Build Cache)是 Gradle 6.0 之后的大杀器。但很多团队开了它却感觉不到效果。为什么?

  • 任务输入中包含了时间戳、路径(比如 file.lastModified() 作为输入)
  • 使用了 SNAPSHOT 或动态版本(Gradle 认为每次都可能变,于是跳过缓存)
  • 跨机器缓存失效(比如 Jenkins 和本地开发机的 JDK 版本不同)

我记得有一次,本地构建只需要 20 秒,但 CI 上却要 2 分钟。后来发现 CI 的构建缓存目录每次都被清空,而且 CI 节点没有配置远程缓存——等于白忙活。

2.6 插件与脚本性能 —— 第三方插件拖后腿

ShadowsPlugin、ButterKnife、Dagger、WMRouter……这些插件在早期版本大多有性能问题。尤其是在配置阶段(Configuration Phase)进行大量文件扫描的插件,会让你的构建还没开始跑 Task 就已经花了半分钟。

怎么判断?看瀑布图上的「Configuration」区域。如果它占一块很宽的色条,那大概率是某个插件在「懒加载」上没做好。

// 一个典型的配置阶段耗时插件的痕迹:
// 在 build.gradle 里大量使用 project.afterEvaluate { ... }
// 或者直接遍历 project.subprojects 没做条件过滤

2.7 汇总:一张图看清六大痛点

构建耗时常见痛点 · 六大类别 构建总耗时 依赖解析 · 动态版本 任务配置 · 串行化 增量编译 · 全量跑 资源处理 · aapt2 缓存未命中 · 远程失效 插件脚本 · 配置阶段 诊断方法:利用网络瀑布图定位耗时阶段 每个痛点通常对应瀑布图上的特定色块和宽度模式

上面这张图把最常见的问题串了起来。你会发现,几乎每个痛点都能在瀑布图上找到对应的「幽灵条」。比如依赖解析对应的是密集的 HTTP 小条,资源处理对应的是又粗又长的 aapt2 条。


2.8 总结一句话

构建慢大多是「组合病」——很少只有一个原因。但如果你能先把上面六个方向筛一遍,基本上 80% 的瓶颈都能定位出来。剩下的 20%,就需要用瀑布图逐帧分析了,那就是咱们后面章节的活儿。