构建为什么越来越慢

一个中型安卓工程的全量构建可能长达数分钟,其中真正编译代码的时间往往只占一小部分,其余消耗在:解析与下载依赖、处理注解、打包资源、执行自定义任务,以及每次构建都要重新计算的配置阶段。

Gradle 的构建过程分为三个阶段:初始化(决定参与构建的项目)、配置(执行所有 build script,构建任务图)、执行(真正跑任务)。优化要分清是哪个阶段慢,因为它们的解法完全不同。

先测量再优化。带 --profile 或构建扫描跑一次,看清时间花在哪,再决定改什么——凭空猜测往往优化错了地方。

下面按依赖、执行、配置三条线索展开,最后给出一份可直接套用的配置。

依赖解析与仓库缓存

依赖是构建阶段最常见的耗时来源,尤其在网络不稳时。核心思路是:让依赖更快被找到,并且更少被重新下载。

  • 声明明确的版本,避免动态版本(如 1.+):动态版本每次都要检查远程仓库,且构建不可复现。
  • 用 dependencyResolutionManagement 统一仓库配置,避免每个模块各写一份导致重复查询。
  • 启用依赖缓存与离线模式:网络受限时可以加 --offline 复用本地缓存。
  • 减少不必要的依赖传递:用 implementation 而非 api,避免把内部依赖暴露给上层模块、扩大重编译范围。

api 与 implementation 的选择影响很大:api 声明的依赖会参与上层模块的编译,改动它会引发更大范围的重编译。除非确实是模块对外接口的一部分,否则一律用 implementation。

增量构建与注解处理

增量构建的目标是“只重做受改动影响的任务”。Gradle 与安卓插件都支持增量编译,但前提是你的自定义任务正确定义了输入输出。

影响增量的四件事
  • 任务输入输出自定义任务必须声明 @Input/@Output,否则每次都被视为脏任务全量执行。
  • 注解处理器每个处理器都会显著增加编译时间,能不用就不用;kapt 比 KSP 慢得多,能用 KSP 就换。
  • 非增量注解部分处理器不支持增量,会让整个编译退化,注意编译日志中的提示。
  • 资源处理关闭不必要的 PNG 压缩与矢量转换,可减少资源处理时间。
构建优化示意:水平延伸的半透明构建管道内流动的发光粒子穿过三个咬合齿轮,右端连着一个分段进度条
构建流水线:决定速度的不只是机器性能,更是任务依赖与缓存设计。

一个立竿见影的改动是把 kapt 迁移到 KSP:在同时使用房间(Room)等注解框架的项目里,编译时间通常能缩短三成以上,而且迁移成本通常只有改几行配置。

配置缓存:跳过配置阶段

每次构建都要重新执行所有 build script 来构建任务图,这在多模块工程里可能就要好几秒。配置缓存(Configuration Cache)把这一步的结果缓存起来,下次直接复用。

启用方式很简单:在 gradle.properties 中打开开关。但它对构建脚本有要求——不能在配置阶段访问项目运行时的状态、不能依赖绝对路径、不能在配置期执行 IO。刚开始启用时通常会报一批错,逐条修完,收益非常可观。

与之配套的还有任务缓存(Build Cache):把任务输出缓存到本地目录甚至远端,多人协作时可以直接复用别人构建过的任务产物。CI 环境尤其值得开启,配合远端缓存能让流水线时间大幅下降。

构建扫描:找出瓶颈任务

# 生成构建概览(本地)
./gradlew assembleDebug --profile

# 输出各阶段耗时摘要(不看可视化报告)
./gradlew assembleDebug --profile --scan

# 查看为什么某个模块每次都重新构建
./gradlew assembleDebug --info | grep -i "not up-to-date"

# 单独测量配置阶段耗时
./gradlew help --profile

构建扫描会给出每个任务的耗时排序、缓存命中情况以及“为什么任务不是最新的”的具体原因。多数情况下,排在前几名的任务只有两三个,优化它们就够了。

常见的发现包括:某个模块的注解处理拖慢了整个构建;某个自定义任务因为未声明输入而每次全量执行;资源处理在每次改动 Kotlin 代码时都被重新执行。这些问题一旦定位,修复成本通常很低。

变体与按需构建

安卓的构建变体(Variant)会按 productFlavors × buildTypes 组合生成任务。变体越多,任务图越大,配置与执行时间越长。

  • 开发期只构建需要的变体:./gradlew assembleDebug 指定具体变体,不要让 IDE 构建全部。
  • 减少不必要的 flavor 维度,能合并的合并;用 flavorDimensions 精简组合数量。
  • 用 androidComponents 的变体 API 按需启用任务,而不是在 afterEvaluate 里动态改依赖。
  • 开启按需配置(Configure on Demand)在多模块工程中可减少配置阶段的工作量。

另一个容易被忽略的点是测试任务:./gradlew build 会连带跑 lint 与所有单元测试,日常开发用 assembleDebug 就够了,把完整检查留给 CI。