
自从接手这个 Android 项目的构建之后我最大的感受就是“等”。改一行文案等 10 分钟动一个资源文件又等 10 分钟明明只是一个 if 条件调整却要盯着 Gradle 的进度条发呆。团队里大家私下都在吐槽每天真正写代码的时间可能只有两三个小时剩下的时间全在陪编译器“默哀”。后来我痛下决心花了两周时间专门啃构建优化把一次全量编译从 10 分钟逐步压到 10 秒级别。这个数据听起来像标题党但它真的不是概念噱头而是对构建流程做了系统性拆解之后实打实的结果。这篇博客不聊玄学不整虚的把我踩过的坑、用过的配置、验证过的方案全部沉淀下来给同样被 Android 构建折磨的朋友一条能直接照做的路。1. 项目概述与目标拆解1.1 我们到底卡在哪这个项目是一个历史包袱比较重的业务型 App模块数量不算夸张但每个模块之间的依赖关系比较乱。最开始接手时我的第一反应是“机器性能不够”于是升级了 CPU、加了内存、换了固态硬盘结果发现冷启动编译时间确实有一点点改善但离预期差了十万八千里。真正的问题并不在“跑得慢”的电脑上而在构建配置本身Gradle 默认参数没调、增量编译没吃到、依赖解析频繁重复、大量不必要的任务在每次构建中都老实执行一遍。这些因素叠加起来10 分钟的编译时长就成了常态。我拿到项目之后做的第一件事不是急着改配置而是先回答三个问题第一这 10 分钟到底花在哪里第二哪些时间可以省哪些时间不能省第三优化到什么程度算是“够用”。只有先把这三个问题搞清楚后面的动作才不会变成瞎折腾。其中最关键的一个认知是我们需要优化的不是“某一次构建的速度”而是“整个迭代循环的平均耗时”这里面的差别非常大。1.2 优化目标如何量化很多人一提构建优化就只盯着“全量编译时间”但实际开发场景中全量编译并不是最频繁的操作。日常写代码更多是改一个 Java/Kotlin 文件、加一个资源、调整一下布局然后点击 Run 看效果。这类操作的耗时取决于增量编译链路是否足够短。所以我的量化目标最终分成两个冷启动全量编译从 10 分钟压到 3 分钟以内这是阶段目标。改动一行代码后的增量编译从 1-2 分钟压到 10 秒级别这是核心目标。这里有个容易被忽略的点Android 项目的编译链路比普通 Java 项目长很多它不只是把 .java/.kt 编译成 class还要跑资源合并、AAPT2 资源编译、dex 化、打包、签名、安装等一系列任务。任何一个环节出现全量执行整个耗时就会被拖垮。所以“10 秒”并不是一个不可能的数字而是意味着我们能够精准地把这些环节中真正需要重跑的部分控制到最小。要做到这一点首先得有精确的诊断数据而不是靠感觉去猜。2. 瓶颈诊断与优化思路2.1 一份 profile 报告理清时间都去哪了我强烈建议所有做构建优化的同学第一步用 Gradle 自带的 profile 功能把构建过程完整记录下来。操作很简单在项目根目录执行./gradlew assembleDebug --profile构建结束后会在build/reports/profile目录下生成 HTML 报告里面会按阶段列出每个任务耗时也能看到依赖解析、配置阶段、任务执行阶段各自占了多少时间。我第一次拿到这份报告时发现一个很扎心的事实真正花在“编译源码”上的时间只占一部分大量的耗时被“配置阶段”和“无意义的任务重跑”吃掉了。举个例子项目里很多模块每次构建都要执行lintVitalRelease、processDebugResources之类的任务但我们在日常调试时根本用不到它们。这种“任务浪费”比单文件编译慢更值得优先处理因为它的可优化空间最大。拿到数据之后我就做了一个简单的任务耗时排序表把所有超过 1 秒的任务全部列出来然后逐个问一个问题这次构建真的需要执行它吗如果不需要就把它跳过如果需要再看是否有办法让它执行得更快。这种“按数据逐个击破”的方式比网上所谓“万能优化脚本”要可靠得多因为每个项目的耗时结构都不一样。2.2 常见慢根因清单根据我的经验Android 项目构建慢的常见原因基本逃不出这几类根因类别典型表现排查方法Gradle 配置阶段慢每次构建都消耗大量时间初始化脚本、解析依赖查看 profile 里 “Configuration” 耗时尝试开启 configuration cache依赖解析重复换个分支、改个依赖版本后重新下载/解析许多包检查网络代理、仓库镜像确保依赖缓存文件夹稳定增量编译失效改一个文件却把整个模块重编检查 Kotlin/Java 增量编译开关确认 annotation processing 配置资源处理重复每次构建都执行完整资源编译和合并检查 AAPT2 相关任务是否常跑考虑资源优化选项后台任务干扰lint、单元测试、build 相关检查任务被意外触发用-x参数跳过或者在 build 变体中按需配置把这张表打印出来的那一刻我心里基本有数了。这个项目的配置阶段之所以慢是因为模块多、依赖图复杂而且不少模块直接在构建脚本里写了动态版本号Gradle 每次都要去远程仓库解析。增量编译失效的问题则主要出在自定义注解处理器上它对增量编译的支持很差导致 Kotlin 编译器每回都要重新扫描大片源码。资源处理慢则是历史遗留的模块结构问题后面再单独处理。2.3 同一条路上的不同走法同样是构建优化外界流行的方案其实很多比如用更高版本的 Gradle、换构建系统、上 Bazel但这些都属于“大换血”风险和成本都极高。对一个正在稳定迭代的业务项目来说最稳妥的方案是“渐进式优化”在不破坏现有结构的前提下先把 Gradle 参数调到位再启用各种缓存和编译增量能力最后才考虑模块拆分和结构治理。我的判断标准很简单哪个改动风险低、收益高就先做哪个。改gradle.properties里的内存参数和构建开关属于低风险高收益可以第一时间做。启用构建缓存和配置缓存属于中等风险但收益同样可观。而把模块进行结构性拆分虽然长期收益最好但牵涉到代码迁移和回归测试周期长、坑也多必须排在最后不能一上来就动。这个优先级的确定直接决定了我们的整体优化路径先把“壳”的问题解决再把“骨头”的问题理顺最后才是“肉”的精细打磨。接下来我讲的每一步实操基本都遵循这个顺序。3. 关键优化实操3.1 gradle.properties 调参最容易见效的一步gradle.properties是我在所有优化落地过程中最先动手的文件。很多项目用好几年这个文件还是默认状态里面就几行注释这等于让 Gradle 开着一辆没调过座椅的车跑长途当然累。我们项目最终沉淀下来的核心配置如下org.gradle.jvmargs-Xmx4g -XX:MaxMetaspaceSize1g -Dfile.encodingUTF-8 org.gradle.paralleltrue org.gradle.cachingtrue org.gradle.configureondemandtrue kotlin.incrementaltrue kotlin.compiler.execution.strategyin-process android.enableJetifiertrue android.useAndroidXtrue这里特别注意org.gradle.jvmargs这一项。很多人以为内存越大越快于是直接给了 8G 甚至 16G后果是构建进程频繁 GC反而拖慢速度。合理的做法是给 Gradle 进程一个“够用但不撑”的内存值4G 到 6G 往往是最稳的区间。同时MaxMetaspaceSize要单独设置否则注解处理和 Kotlin 编译这类吃元空间的操作容易 OOM。org.gradle.paralleltrue的作用是让多个模块并行构建这对多模块项目收益非常明显。但它有一个前提就是模块之间的依赖关系必须清晰否则并行反而会导致任务等待和资源争抢。我们项目之前模块依赖纠缠不清开启并行后反而出现了奇奇怪怪的死锁问题后来我把依赖理顺才真正吃到并行构建的红利。configureondemand更像是“按需配置”只对需要的模块执行构建脚本配置能省下配置阶段的大量时间但它同样要求模块边界干净。3.2 构建缓存与本地缓存把重复劳动消灭在源头Gradle 的构建缓存是另一个绕不开的大头。从 3.5 版本开始构建缓存就能把 task 的输入输出缓存下来同一份输入在下次构建时直接命中缓存不再重复执行任务。这个特性最典型的受益场景就是资源处理和 dex 化只要资源文件没变processDebugResources的结果就能直接复用。在我的优化方案里我在gradle.properties里加了org.gradle.cachingtrue然后又保证项目的buildCache配置指向了一个稳定的本地目录。代码修改通常涉及编译任务本身但资源、Manifest 处理、Dex 打包这些重活在缓存命中时就能飞速跳过。具体配置可以放在项目的build.gradle顶层或 settings 里buildCache { local { enabled true directory new File(rootDir, build-cache) removeUnusedEntriesAfterDays 7 } }这里要提醒一句构建缓存听起来很香但它不是默认就能“命中”的。如果项目里有大量不稳定的 task比如动态生成文件、时间戳参与输入、输出路径随配置变化那缓存命中率会低到让人怀疑人生。我的经验是不要指望一次性把所有 task 都吃到缓存先把资源类、打包类这些核心重活调到命中就已经能省下大量时间了。另外本地缓存目录记得加入.gitignore千万别提交到版本库里。3.3 Kotlin 增量编译与注解处理别让一行改动牵动整个模块这个项目有一半以上的代码是 Kotlin 写的Kotlin 编译速度直接决定了增量构建的最坏情况。在 Gradle 里Kotlin 增量编译默认是开启的但真正拖后腿的往往是注解处理器。我们项目用了 Room、Dagger 这类需要 kapt 的框架而 kapt 天生就对增量编译不友好尤其是 Dagger它生成的代码量巨大任何一个伴生对象里的小改都可能触达大面积重新生成。为了应对这个问题我的做法分三步走。第一步确认kapt的增量配置没有被人为关闭也就是前面gradle.properties里写的kotlin.incrementaltrue一定要显式声明因为某些 Gradle 版本对增量支持默认是开着的但 kapt 的useBuildCache和correctErrorTypes配置也会影响增量效果。第二步用kotlin.compiler.execution.strategyin-process把编译放在 Gradle 进程内省去反复启动 Kotlin daemon 的开销。第三步也是最推荐的一步看项目里能不能把部分依赖从 kapt 迁移到 KSP。KSP 是 Kotlin 官方支持的注解处理方案它在设计上对增量编译更友好速度通常能比 kapt 快一倍以上。不过迁移需要改动不少构建脚本和依赖不是一两周就能完成的。所以我的顺序是先通过构建缓存和参数调整把现状压到可接受范围然后再找时间做 KSP 迁移。如果你正在一个新项目里真的建议一开始就选 KSP别等历史包袱攒够了再痛苦迁移。3.4 配置缓存让“开始构建前”的等待也彻底消失前文我提过这个项目的配置阶段经常消耗整次构建 20% 以上的时间。Gradle 为了执行一个构建任务需要先解析所有模块的构建脚本、计算依赖图、创建任务关系这套动作每天要被我们反复触发几百次。配置缓存Configuration Cache就是把这部分计算结果序列化下来下次构建直接复用跳过配置阶段。这真的是一个“让构建从 1 分钟变成 10 秒”级别的优化但代价是需要项目里的构建脚本满足一定的约束。启用方式很简单在gradle.properties里加一行org.gradle.configuration-cachetrue然后重新跑构建。如果你的项目恰好兼容你会看到日志里出现“Configuration cache entry stored”这样的提示下次构建配置阶段会大幅缩短。但我必须很直白地说老项目大概率不会一次就兼容。常见的坑包括构建脚本里用了System.currentTimeMillis()、读取了环境变量、动态写文件等操作。Gradle 会把不兼容的地方直接报出来你需要一个个去修把这些副作用改成惰性计算或者彻底移除。我们当时花了两三天梳理了所有不兼容点最折腾的是一个模块在配置阶段做了网络请求判断版本信息这个操作几乎注定无法兼容配置缓存。最终我们把动态逻辑移到了运行时或者改成通过任务执行配置阶段只保留静态信息。改完之后配置缓存带来的收益非常可观冷启动构建里配置阶段从十几秒直接压到不到一秒整个构建提速是很明显的。3.5 模块与依赖治理把“结构性浪费”彻底清掉前几节讲的都是参数和缓存层的优化属于“把系统调到最佳状态”。但如果项目本身结构有问题比如模块之间循环依赖、一个大模块里塞了太多无关代码那再怎么调参都有上限。我们在前面几轮优化之后构建时间已经降了很多但离 10 秒目标还有距离。真正让我们完成临门一脚的是一个看似不起眼却影响巨大的改动把几个大模块做了懒加载改造和依赖清理。这个项目原本有一个基础模块几乎被所有业务模块直接依赖里面塞了网络、图片、工具类、UI 组件甚至部分业务逻辑。任何代码只要动到这个模块其他所有依赖它的模块都要重编。我们在分析 profile 时发现改动频繁地集中在几个业务模块但因为基础模块被牵连每次都是整片重编。后来我做了两步第一步把里面真正无状态的纯工具类抽成一个core-util模块这个模块几乎不变专门吃缓存第二步把业务相关的逻辑向外移动到具体业务模块减少基础模块的职责。这两步做完后增量编译的命中范围瞬间缩小单次编译自然就快了。依赖版本方面我也把所有动态版本号统一改成了固定版本。动态版本号会让 Gradle 在每次依赖解析时都去仓库检查一遍虽然不一定重新下载但解析本身就要消耗网络和 CPU 资源。改成固定版本后依赖解析阶段几乎不再成为瓶颈。这一步也不是什么高深技术却是很多团队忽略的“脏活”效果往往最直接。4. 常见问题与排查技巧实录4.1 缓存命中率低到离谱先查输入指纹如果你开了构建缓存但觉得速度没变化别急着怀疑缓存没用先确认任务输入是不是稳定的。最常见的“缓存杀手”是构建脚本里有人写了类似def buildTime new Date()这样的代码并且这个变量参与了某个任务的输入。因为每次时间都不同任务输入指纹自然不同缓存永远不命中。排查方法很简单跑一次带--info的构建看到某个任务输出“CACHED”或“FROM-CACHE”的次数占比就能判断。如果资源处理类任务始终在重跑重点看它的输入是否覆盖了不稳定的文件或属性。我当时就发现有一个自定义任务把project.buildDir整个目录当作输入这个目录里全是增量产物每次构建都会变相当于整个缓存设计白搭。把输入收敛到真正有意义的源文件之后缓存命中率立刻恢复正常。这个坑很隐蔽希望第一次做缓存优化的人能提前避开。4.2 配置缓存报错任务却还是能构建别麻木地关掉它开启org.gradle.configuration-cachetrue之后项目可能会在某个任务上报出一堆警告但 Gralde 出于兼容性考虑有时会退化到“无法缓存配置”的模式然后继续执行构建。这时候如果只看“构建成功了”就收手其实配置缓存根本没生效。正确做法是仔细阅读警告内容定位到具体是哪段构建脚本在配置阶段执行了不该执行的逻辑。我们的处理流程是先把所有警告整理成一个清单然后逐项把不兼容的逻辑改成任务执行时的动作或者用provider {}延迟求值。修改的过程中可能会出现新的警告这很正常构建脚本里的隐含依赖不是一两轮能清干净的。经修改后后可以连续执行两次构建第二次看到配置阶段耗时显著下降才说明配置缓存真正生效。这个动作必须当成一次“专项整改”来做不能说改两处就放弃。4.3 并行构建导致的资源争抢与死锁开启org.gradle.paralleltrue后多模块项目确实能加快整体构建但如果模块数量很多、CPU 核心线程数设得过大会反过来变成“先快后慢”任务并行执行的前半程 CPU 满载后半程频繁等待磁盘 IO总时间反而上升。我自己遇到过一种情况16 核机器上默认并行线程数太大构建日志里出现了大量的“WAITING”状态最后我把并行线程数手动限制为 8整体速度才恢复正常。死锁问题则多半来自模块之间的循环依赖只要你用project方式在构建脚本里直接引用其他模块的某些资源或类就容易埋雷。遇到死锁时不要只盯着并行参数改而是要回头理模块依赖图把环断开。这属于结构问题参数怎么调都救不了。4.4 改了资源文件整个模块还是全量重编资源文件的改动不及时触发增量通常是因为 Resources 任务把模块里的res目录或assets目录整体当作输入任何一个文件变化都会重跑整套资源处理链路。针对这一点我能想到的有限手段是把真正需要动态变化的资源隔离到独立模块或独立目录并减少资源引用层的耦合。对于大部分项目来说最实用的技巧是避免在res/values中频繁改文件因为 values 下的翻译和资源项一旦变化AAPT2 往往会重编大量的资源索引。如果你的项目里图标、切图这类静态资源很多可以考虑用androidResources的配置把这些资源单独压缩处理减少每次构建的扫描范围。说实话这部分能优化的空间有限毕竟资源合并本身就是 Android 构建链路里绕不开的一环但至少可以避免因为“不小心”造成的不必要全量。先把不必要的重跑控制住资源构建整体的压力就已经小了很多。5. 从 10 分钟到 10 秒背后的经验沉淀5.1 本地缓存与团队级缓存的分工聊完了具体配置我想再讲讲缓存体系的分层。很多人以为构建优化就是“在本地把代码编译快一点”就完事了但真实工作中的痛点还包括换台电脑、切换分支、跑 CI 时重新踩同样一遍坑。本地构建缓存解决了单机重复劳动但你没法保证团队成员各自机器上的缓存一致也没法保证 CI 机器能复用大家的成果。所以更完善的方案是在团队内部搭建一个远程构建缓存服务或者至少把 CI 上产出的缓存定期同步到一个共享存储里让开发者拉下来直接使用。我当时的项目体量还没到必须搭远程缓存的程度但我把 CI 构建的缓存目录备份到一台内网服务器的固定位置新入职同事在首次构建前把这些缓存手动恢复一下冷启动时间一下就少了一大半。如果你的团队规模更大直接上 Gradle 的远程缓存HTTP 缓存托管会更好只是需要注意缓存内容的有效性和安全性别让团队的私有中间产物被无关人员轻易拿到。5.2 构建配置治理功夫在配置之外优化到后来我有一个越来越明显的感受构建速度只是结果前面的配置质量才是根因。项目里的build.gradle如果写得像“屎山代码”各种动态逻辑、无意义依赖、全局扩展满天飞那任何参数调整都只是表面粉饰。构建脚本本质上也是代码应该有代码评审、有依赖治理、有版本锁定甚至应该定期来一次“构建脚本清理日”。我把团队的构建脚本调整成了一个相对稳定的状态所有模块遵守统一的配置模板版本号集中管理不必要的插件只在需要它的模块里声明全局配置尽量少用。这些规则听起来不高深但它们带来的稳定性非常明显。后来的新成员写编译配置时也更少出错构建链路自然维持在优化后 10 秒级别。5.3 一些值得长期坚持的构建习惯最后分享几个我觉得特别适用的实操习惯它们不是某一次优化时的临时动作而是我长期坚持的工作方式每次重大依赖或 AGP 版本升级后务必重跑./gradlew clean assembleDebug并记录耗时变化避免版本升级带来的隐形回退。提交代码前用--profile生成一份构建报告扫一眼有没有异常耗时的任务比等人反馈“构建越来越慢”再排查要省力得多。出现“构建变慢”苗头时先查是不是有人往构建脚本里加了动态时间戳、随机文件名或者临时的println大多数诡异问题都是这类小改动引起的。在团队文档里沉淀一份“构建优化速查表”把常见的配置项、命令和排查思路写清楚不要等新人踩坑后才后悔。在这个项目之前我一直觉得“编译优化”是一件门槛很高的事可能要到系统源码级别才能玩得动。但踩过这些坑之后我发现对大多数业务项目而言收益最大的恰恰是那些“细节”和“常识”合理的内存参数、高质量的缓存命中、干净的模块依赖。把这些做到位构建速度自然会回馈你。改一行代码等 10 秒这种爽感真的只有体验过的人才知道而且一旦体验过就再也不想回到笨重构建时代了。