如何分析 Gradle 构建耗时并定位慢任务?

解读

在国内 Android 工程里,Gradle 构建动辄 5~10 min,CI 排队半小时是常态。面试官问“怎么找慢任务”,不是让你背几条命令,而是考察:

  1. 能否把“体感慢”量化成可追踪的指标;
  2. 能否把指标拆到“哪个阶段-哪个任务-哪段代码”;
  3. 能否给出落地套路(脚本、CI 缓存、远程仓库、KPI 对齐)。

一句话:既要能“看病”,也要能“开方”。

知识点

  1. Gradle 生命周期:Initialization → Configuration → Execution;国内常见“Configuration 过慢”是 KTS + 多模块 + apply from 乱用导致。
  2. Build Scan、Gradle Profiler、Gradle Doctor、Task 级 --profile 报告;国内网络下必须配置代理仓库(阿里云、腾讯、华为)否则下载依赖占大头。
  3. Task 输入输出快照(TaskInputs/Outputs)、增量编译(IncrementalTask)、构建缓存(BuildCache)、并行执行(--parallel)、守护进程(Daemon)、配置缓存(Configuration Cache)。
  4. 国内特色卡点:AAR 多维度 flavor、字节码插桩(APM、埋点、热修)、Kotlin KAPT 注解(Room、ARouter、Hilt)、Transform API 与 AGP 7.x 的 DexBuilder 合并、R 文件生成、资源合并(ResourceMerger)耗时。
  5. 定位套路:先整体(Scan)→ 再阶段(profile)→ 再任务(task --info)→ 再代码(自定义 Plugin 插桩)→ 最后治理(缓存、增量、降级、拆模块)。

答案

我把它拆成“三步七招”,线上与 CI 通用,全部在现网验证过。

第一步:量化

  1. 本地开发
    打开 gradle.properties:
    org.gradle.daemon=true
    org.gradle.parallel=true
    org.gradle.configureondemand=true
    org.gradle.caching=true
    执行:
    ./gradlew assembleDebug --profile --scan --info
    构建结束后生成 build/reports/profile/timestamp.html,同时控制台给出 Build Scan 链接(国内需配置代理仓库,否则上传失败)。
  2. CI 量化
    在 GitLab-CI 或 Jenkins Pipeline 里加:
    ./gradlew assembleDebug --build-cache --scan -Dorg.gradle.workers.max=8
    把 scan 结果 url 写到 PR 评论,方便回溯。

第二步:定位 3. 看阶段耗时
打开 profile.html,先对比 Configuration、Dependency Resolution、Task Execution 三栏。国内项目最常见“Configuration 30 s+”,90% 是 KTS 里 allprojects 滥用、apply from 远端脚本、repository 顺序不合理。
4. 看任务耗时
在 Task Execution 页按“Own Duration”倒序,锁定前 10 个任务。
常见慢任务黑名单:

  • kaptDebugKotlin:KAPT 生成代码,注解处理器太多。
  • mergeDebugResources:资源过多,且未启用 resourcePrefix。
  • transformClassesWithXXXForDebug:字节码插桩插件(SensorsAnalytics、Bugly、热修)。
  • dexBuilderDebug:AGP 7.x 把多 DEX 合并放这里,模块太多、依赖爆炸。
  1. 看重复任务
    用 Gradle Doctor 插件:
    plugins { id "com.osacky.doctor" version "0.8.1" }
    运行后控制台会打印“Tasks that should be UP-TO-DATE but took >1 s”,直接告诉你哪些任务本可以跳过却被强制重新跑。
  2. 看缓存命中率
    在 Build Scan → Performance → Build Cache 里,若“Cache misses”>20%,重点检查:
    • 任务声明了 @TaskAction 但每轮输入快照不同(例如用了 new Date()、System.currentTimeMillis());
    • 任务输出路径带版本号,导致 key 永远对不上;
    • 依赖仓库 SNAPSHOT 频繁刷新。

第三步:治理 7. 对症下药

  • KAPT 慢:升级 Kotlin 1.8+,启用 kapt.use.worker.api=true、kapt.incremental.apt=true;把 ARouter、EventBus 换成 KSP。
  • 资源合并慢:开启 android.enableResourceOptimizations=true,给每个模块加 resourcePrefix,禁用 pngcrunch(android.enablePngCrunching=false)。
  • Transform 插桩慢:把 SensorsAnalytics 的 transform 改成 ASM API 增量模式,或把插桩下沉到 CI 的 Release 包,Debug 包直接关闭。
  • 模块太多:用 gradle-include-guard 脚本,按需 apply 子工程;把 60 个模块拆成 20 个 aar-publish,二进制依赖替代源码依赖。
  • 远端脚本慢:把原来 apply from: 'https://xxx.gradle' 下载到本地 includeBuild,CI 缓存目录挂载到 ~/.gradle/init.d。
  • CI 缓存:在 Jenkins 里开“Pipeline Maven/Gradle 缓存插件”,把 /.gradle/caches、/.gradle/wrapper 做 Volume 缓存;GitLab-CI 用 cache:key: files: ['gradle/wrapper/gradle-wrapper.properties']。

落地效果:我们 220 万行代码、78 模块的工程,本地 clean build 从 11 min 降到 3 min 20 s,CI 平均缩短 6 min,每周节省 200+ 核时,KPI 直接对齐“构建效率提升 60%”。

拓展思考

  1. 配置缓存(Configuration Cache)与 Kotlin DSL 的兼容坑:国内很多老插件用 Project.afterEvaluate 访问 Task,导致“CC”无法启用,需要插件作者改造成 Task 注册时惰性配置。
  2. 远程构建缓存:Google 推荐用 Gradle Enterprise,但国内机房访问延迟高,可自建 MinIO + nginx 做 S3 兼容缓存,成本仅为 Enterprise 的 1/10。
  3. 版本升级 ROI:AGP 8.0 要求 JDK 17,CI 镜像要整体换,如果团队还在 JDK 8,需要评估“升级带来 30% 构建提速”与“全员换 IDEA、换镜像”之间的人力成本。
  4. 未来趋势:Gradle 正在推“Isolated Projects”,把每个 project 做成沙箱,彻底解决 Configuration 阶段的全局锁,一旦稳定,国内多模块巨型 App 的 Configuration 耗时有望再降 50%。