build scan的gantt图可直观识别串行瓶颈:关键路径上宽度显著(>60秒)、无并行重叠、下游任务统一在其结束后启动、状态为executed的“独占型”任务即为排队根源。

怎么看任务时间线里哪些任务在“排队”
Build Scan 的 Task Execution 时间轴不是简单罗列耗时,而是按实际执行顺序(含依赖约束)横向铺开。真正卡住构建的,往往不是单个慢任务,而是某个长任务挡在关键路径上,导致后续大量任务被迫等待——这种“串行瓶颈”在时间轴上表现为:一条长条任务横跨整个视图,下方大片空白区域里本可并行的任务全被压扁成竖线、挤在它结束之后才开始。
注意区分两种“长”:minifyReleaseWithR8 耗时 3 分钟是自身计算重;而 compileDebugJavaWithJavac 耗时 20 秒但后面跟着 15 个模块的 test 任务全部从它结束后才启动,这才是串行瓶颈。
识别关键路径上的“独占型”任务
打开 Build Scan 报告 → Performance → Task Execution → 切换到 Gantt Chart 视图。重点看满足以下条件的任务:
- 宽度明显超过其他任务(比如 > 60 秒),且左右无重叠的并行任务
- 它的结束时间点,恰好是下方一大片任务统一的“起始时间”
- 鼠标悬停时显示
reason: depends on ...,但依赖链上游只有它一个“宽任务” - 状态为
EXECUTED而非UP-TO-DATE或FROM-CACHE——说明每次都在真实拖慢流程
典型例子:processDebugResources 在老版本 AGP 中常因 AAPT2 启动开销大、无法有效复用而成为关键路径堵点;transformClassesAndResourcesWithR8ForDebug 在未启用 R8 缓存时也会反复独占 CPU。
为什么 --profile 报告不如 Build Scan 直观
./gradlew assembleDebug --profile 生成的 HTML 报告只展示任务耗时排序和配置阶段分解,但不还原真实执行时序。它告诉你 test 总耗时 120 秒,却看不出这 120 秒是 12 个模块各跑 10 秒(并行),还是 1 个模块跑了 120 秒(串行)。而 Build Scan 的 Gantt 图直接暴露了调度事实——Gradle 内部的 DAG 执行引擎是否真把可并行任务分发出去,一眼可见。
另外,--profile 不标注任务状态(UP-TO-DATE / FROM-CACHE),也看不到输入输出变更详情,很难判断“为什么这个任务不能跳过”。串行瓶颈常源于不该执行的任务被强制执行,这点只能靠 Build Scan 定位。
确认并验证串行假设的实操动作
发现疑似瓶颈任务后,别急着改代码。先做三件事验证是否真是串行问题:
- 运行
./gradlew --dry-run assembleDebug,观察该任务是否出现在执行计划中——如果连 dry-run 都要等它,说明依赖关系写死了 - 检查该任务的
inputs和outputs:用./gradlew help --task <taskname></taskname>看声明,再查源码或插件文档,确认它是否真的必须等上游全部完成(比如某些资源合并任务要求所有模块编译完才能开始) - 临时注释掉部分子模块的
apply plugin: 'com.android.application',看该瓶颈任务的起始时间是否前移——如果前移,说明它之前确实在等那些模块
很多团队花时间优化 javac 参数,结果发现真正卡住的是 generateDebugBuildConfig 因为依赖了一个全局静态常量文件,而那个文件被所有模块共同引用——这种隐式串行依赖,只有在时间轴上拉满缩放才能看清。











