尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Android构建优化实战:从10分钟到10秒的Gradle调优

Android构建优化实战:从10分钟到10秒的Gradle调优 做 Android 构建优化这件事听起来好像是架构组才需要干的活但真正被编译折磨过的人才知道一个能快速跑起来的构建流程比多导几个框架都更能提升效率。我接手的一个项目之前打 debug 包经常要 10 分钟起步改一行 Kotlin 重新跑一次光是“编译 装到模拟器”就能耗掉一小节会。后面我花了一周时间把 Gradle 配置、AGP 版本、模块依赖、缓存策略和日常开发姿势全捋了一遍最终把日常增量编译压到了 10 秒左右。这篇分享不是教你把所有构建都做成“秒过”而是给你一套从定位问题到落地优化的可复现路径。适合那些已经熟悉 Android 工程结构、但一直没系统调过构建速度的人参考。先说结论如果你每次编译前还在坚持clean那 10 分钟一点都不冤。我们真正应该在意的是“改一行代码之后重新跑起来”要多久。这篇文章里提到的所有操作都没有用任何黑科技也没有换构建系统只是把 Gradle 和 AGP 本来的能力用对了。1. 先弄清楚编译时间都浪费在哪儿1.1 构建阶段拆开看不要凭感觉优化拿到一个编译慢的项目第一件事不是去改gradle.properties而是弄清楚时间到底耗在哪个阶段。Gradle 构建大致可以分成两段配置阶段和任务执行阶段。配置阶段要解析settings.gradle、build.gradle、gradle.properties然后生成整棵任务图。很多项目配置阶段就占了 1 到 2 分钟多半是脚本里写了太多自定义逻辑比如每个模块都要去读外部配置、动态生成依赖版本、大量使用afterEvaluate这些都会拖慢配置速度。任务执行阶段才是大头里面又分 Kotlin/Java 编译、资源处理、Dex 打包、APK 打包等子任务。每个子任务的耗时都不一样盲目调大内存不一定有用。想看清楚可以用 Gradle 自带的 profile./gradlew :app:assembleDebug --profile跑完之后项目根目录下的build/reports/profile/里会生成一个 HTML 报告按耗时排序展示每个 task。我这边第一次看完报告就发现最慢的并不是 Kotlin 编译而是资源处理相关任务尤其是 debug 包居然开了资源压缩。这个结论靠猜是猜不出来的。Android Studio 用户也可以直接用 Build Analyzer入口在Build - Analyze Build里。它能把同一份构建按配置、task、依赖下载等维度拆开看起来更直观。我自己习惯两者配合先看 profile 报告拿到完整 task 耗时再用 Android Studio 定位具体是哪条配置导致某个 task 频繁重跑。1.2 全量构建和增量构建优化逻辑完全不同先说一个很多人容易混淆的点10 分钟到底是什么构建。如果执行了./gradlew clean assembleDebug那叫全量构建。clean 会把所有中间产物全部删掉整个过程从头来一遍。项目只要稍微大一点全量构建 10 分钟很正常优化空间主要来自并行度和缓存复用。但日常开发根本不会每次都 clean实际上更烦的是“改一个文件然后点 Run”这次构建是增量构建它依赖 Gradle 的 up-to-date 检查来跳过没变化的任务。Gradle 会对每个 task 的输入、输出做内容哈希只要输入没变任务就直接标成UP-TO-DATE或FROM-CACHE。问题在于很多项目配置会让输入看起来“一直变”。比如依赖用了动态版本号比如任务里读了时间戳文件再比如每次编译都会生成随机标识资源这些都会导致增量机制失效让一个小改动触发大量无关任务重跑。理解这一点之后优化目标就清晰了让“改代码”这种高频场景尽量少触发无关任务同时让真正要跑的任务能复用缓存结果。我优化前的项目典型耗时大概是这样的场景优化前clean 后全量构建10~11 分钟改一个 Kotlin 文件后的增量构建2~4 分钟改完布局/资源后的增量构建4 分钟以上后面我会把每一项都往下压尤其是第二项直接压到 10 秒左右。2. 核心参数与工具链选型哪些配置最值钱2.1 先把 gradle.properties 改对gradle.properties是全局构建参数的入口也是最容易立刻见效的文件。很多人只知道加org.gradle.jvmargs-Xmx2g其实工程大一点这个内存根本不够Kotlin daemon 和打包进程都容易频繁触发 GC反而更慢。我这边最终使用的是这套org.gradle.jvmargs-Xmx4g -XX:MaxMetaspaceSize1g -XX:HeapDumpOnOutOfMemoryError org.gradle.paralleltrue org.gradle.cachingtrue org.gradle.configuration-cachetrue android.useAndroidXtrue android.enableJetifierfalse kotlin.incrementaltrue逐个解释一下。-Xmx4g是给 Gradle daemon 的 JVM 堆内存上限注意不要一味调大。如果本机总共 16G 内存还要跑模拟器和 IDE给 Gradle 8G 反而容易触发系统交换实际效果更差。4G 是我在多台机器上试下来比较稳的折中方案。org.gradle.paralleltrue开启多模块并行编译这个对多模块项目收益很明显。另一个和并行容易混淆的是org.gradle.configuration-cachetrue它能让 Gradle 跳过配置阶段、直接复用构建配置结果。配置阶段从几十秒降到几秒就靠它。但 configuration cache 对自定义 Task 和老插件兼容性要求比较高如果开启后出现序列化报错需要逐个排查实在不行就先关掉。android.enableJetifierfalse需要说清楚如果你的项目还依赖老 support libraryJetifier 必须保持开启如果已经全部迁移到 AndroidX那关掉能省下一大块处理时间。我自己这个项目早就切到 AndroidX 了所以直接关掉。2.2 工具链版本别选太老也别盲目追新构建工具链的版本组合对编译速度的影响比大多数人想象得大。尤其是 AGPAndroid Gradle Plugin和 Gradle 版本的搭配关系一定要看官方兼容表。我优化时用的是 Gradle 8.5 AGP 8.2 JDK 17 这套组合。相比老版本AGP 8.x 对构建缓存和配置缓存的支持更成熟默认就开了很多增量优化。JDK 17 又能让 Kotlin 和 Java 编译工具链统一在一个运行时里减少来回切换的开销。这里给一个参考组合组件版本Android Studio2023.2 或更新Gradle8.5AGP8.2JDK17Kotlin1.9.xcompileSdk / targetSdk34如果你还没有升级 AGP建议至少升到能使用 configuration cache 的版本。升级过程中最常见的坑是 Gradle wrapper 版本没跟上导致 AGP 不兼容。改完gradle-wrapper.properties后记得先执行一次./gradlew --stop重启 daemon否则旧 JVM 参数还会残留。2.3 依赖和模块配置里藏着的隐形杀手编译慢的项目往往有一个共同点依赖写得不够“收敛”。第一个杀手是动态版本号。implementation com.something:lib:1.这种写法会让 Gradle 每次构建都去检查远程仓库里有没有新版本特别容易破坏增量构建。哪怕网络很快也会让依赖解析阶段不稳定。我的建议是全部改为固定版本依赖更新统一走 Dependabot 或 Renovate不要写在构建脚本里。第二个杀手是模块间错误地使用api。Gradle 的implementation和api语义差别很大如果 A 模块依赖 B 模块时用了api那么只要 B 的接口签名变化所有依赖 A 的模块都会重新编译。大部分内部模块其实只需要implementation这样能让 Gradle 利用“编译避免compile avoidance”能力只重编真正受影响的上游模块。第三个杀手是产品变体太多。每个 productFlavor 都会让 task 数量成倍增加比如有两个维度各三个 flavor任务组合就是九个。调试机只需要一种组合其他组合纯属每天白跑。如果项目里历史包袱没法去掉至少给debug变体单独留一条轻量路径不要把所有 flavor 的配置都堆到 debug 构建里。3. 实操细节把增量编译从几分钟降到 10 秒3.1 第一步让本地构建缓存真正起作用很多人以为org.gradle.cachingtrue打开了就有缓存实际上它只是开启了“任务输出缓存”能力真正要让缓存起效还需要确保 task 的输入是稳定的。常见的隐患是自定义 task 把时间戳、绝对路径或者随机值当输入这样每次计算的缓存 key 都不同等于没有缓存。一个最直接的验证方法先跑一次./gradlew :app:assembleDebug再跑一次./gradlew clean :app:assembleDebug。如果有了本地缓存第二次 clean 后的构建会从缓存加载大量 task 输出而不是从头再编译一遍。这里的核心逻辑是Gradle 的任务输出缓存可以对“相同输入参数”的历史构建结果复用即使本地中间产物已经被 clean 删除。如果你有 CI 环境还可以进一步搭建远程构建缓存让本地构建直接拉取 CI 在同源码状态下产生的产物。不过个人项目不建议一上来就上远程先把本地缓存跑通收益已经非常明显。3.2 第二步模块拆分是提效不是炫技多模块工程确实能加速构建但不是“拆得越碎越好”。如果模块之间依赖关系混乱改一个底层工具类就能引发一整条依赖链上的所有模块重新编译那还不如单模块跑得快。我见过一个项目把网络层、图片加载、公共 UI 全部单独拆模块看起来挺规整但底层模块改动实际上会触发十几个下游模块重建。后来我做了一轮依赖收敛能只在app里用的依赖就不下沉到底层模块模块之间尽量用implementation而不是api并且把几个关联紧密的模块重新合并。模块化比较好的状态是大部分日常改动发生在少数几个模块里改动只触发很少的编译链。如果某个底层模块经常被改那它要么不够底层要么拆分粒度不对。用这个标准去审视自己的工程比单纯追求模块数量有意义得多。另外如果项目还在用 kapt 做注解处理可以考虑迁移到 KSP。kapt 会拖慢 Kotlin 编译的增量能力而 KSP 在编译速度和增量支持上都好很多。如果暂时没法迁移至少把注解处理相关的模块单独隔离不要让所有 module 都背这个编译负担。3.3 第三步日常开发姿势也决定 10 秒能不能落地配置改完、缓存弄好之后剩下的就是操作习惯问题。很多人编译慢一半原因是“自己手动 clean”。只要你随便翻一眼项目里的 build 目录就会发现中间产物其实是可以复用的没必要每次全删。我日常的开发流程大概是这样打开项目后先手动跑一次./gradlew :app:assembleDebug --offline让 Gradle daemon 和 Kotlin daemon 提前热起来。改代码时只按CtrlF9编译当前模块不要从顶部菜单点 Run更不要 clean。需要看效果时优先用 Android Studio 的 Apply Changes。如果只是改方法体内部逻辑它能做到秒级更新。只有改了 AndroidManifest 或资源结构、新增类等结构性改动时才走完整的 installDebug。这里要特别说一下--offline。它只适合在依赖已经缓存好的情况下使用作用是跳过远程依赖解析步骤减少不必要的网络检查。如果项目刚 clone 下来本地还没有依赖不要加这个参数否则会报找不到依赖。我对比过同一个项目“正常点 Run”和“按这套流程走”的耗时差后者在“改一行 Kotlin 代码再跑起来”的场景下确实可以稳定在 10 秒左右。这个 10 秒不是指 APK 从没构建过到构建完成而是指在增量构建 Apply Changes 这个高频路径上的真实体验。4. 常见问题与排查技巧实录4.1 问题速查表优化过程中很容易踩到一些看起来莫名其妙的问题下面这些是我实际遇到过的组合现象可能原因解决思路改了代码但运行后还是旧逻辑构建缓存命中了旧产物或增量编译没识别到变更先试./gradlew :app:assembleDebug --no-build-cache确认后再查 task 输入开启 configuration cache 后大量报错某个插件或自定义 Task 不支持序列化暂时关掉 configuration cache找出生问题插件或 TaskKotlin 编译每次都全量使用 kapt 或注解处理器导致增量失效迁移到 KSP或把注解处理模块隔离执行 clean 后依然非常慢没有配置好本地 task 输出缓存确认org.gradle.cachingtrue检查自定义 task 输入是否稳定Gradle daemon 内存溢出org.gradle.jvmargs太小或太大先设为-Xmx4g再观察 GC 情况手动杀进程后 Gradle 卡住旧 daemon 还占着文件锁执行./gradlew --stop后重新构建这张表不是标准答案但能帮你快速缩小排查范围。遇到奇怪问题第一反应不要是删 build 目录先加--info参数跑一次看 Gradle 自己输出里有没有线索。4.2 我踩过的几个坑第一个坑是 debug 构建也开了代码压缩。因为某位同事把 release 的混淆配置不小心写进了debug构建类型里导致每次 debug 编译都要跑一遍 shrink 流程。这个问题不仔细看 profile 报告根本发现不了因为它不会报错只会默默变慢。第二个坑是杀毒软件把 Gradle 缓存目录当扫描目标。Windows 环境下安全软件全盘扫描很容易盯上.gradle/caches和项目build目录构建时读写文件速度直接砍半。把这两个目录加进排除列表之后构建时间肉眼可见地下降了一段。这不是玄学是真的会发生。第三个坑是“同步和构建同时跑”。Android Studio 在做 Gradle sync 的时候会抢占资源如果你刚 sync 完立刻点 Run两个 daemon 可能同时在解析工程效果就是卡顿加倍。一个稳妥的做法是让 sync 完全结束之后再构建别急着点那个绿色箭头。4.3 改了配置没效果先重启 daemon 再下结论很多配置改动尤其是gradle.properties里的 JVM 参数不会立刻生效。因为 Gradle daemon 已经在后台运行旧参数还占据着内存。我见过有人改完-Xmx4g跑起来还是 OutOfMemoryError以为是配置写错了其实就是没重启 daemon。正确操作是./gradlew --stop然后重新执行任意构建。后续再做参数调整时最好养成改完配置先--stop的习惯。Android Studio 里的 Gradle daemon 和命令行共用同一套所以命令行杀掉之后Android Studio 下次构建也会新起一个 daemon。5. 最终效果与我的个人体会5.1 实测数据对比整套优化做完之后同一个项目在我本机上的表现变成这样场景优化前优化后clean 后全量构建10~11 分钟3~4 分钟改一个 Kotlin 文件后的增量构建2~4 分钟8~12 秒改布局/资源后的增量构建4 分钟以上20~30 秒配置阶段耗时30~60 秒3~6 秒可以看到“从 10 分钟到 10 秒”最准确的说法是高频的增量编译场景被压到了 10 秒量级。全量构建受限于机器性能和项目体量只能从 10 分钟收敛到 3 分多钟但已经不影响日常开发心情。这套优化还有一个隐藏收益因为构建快了开发阶段更愿意跑实际设备验证效果而不是改完就“凭感觉觉得没问题”。构建链路一旦顺滑整个人的写代码节奏都会不一样。5.2 理性看待构建优化这件事我也想给正在观望的人提个醒并非所有项目都能做到 10 秒。如果你的工程里有大量 C 代码或者依赖了非常重的外部原生库那么 NDK 编译本身就是硬耗时再调 Gradle 也压不到秒级。还有超大资源包项目比如包含几百 MB 本地素材资源压缩那一步也不是缓存能救回来的。这种时候要做的是区分“可缓存的部分”和“不可避免的部分”。可缓存的部分交给 Gradle build cache、configuration cache 和模块化去处理不可避免的部分只能从工程结构层面想办法比如减少原生代码改动频率、把体积大的资源模块独立下沉。我个人在实际操作中最大的感受是构建优化本质上不是让机器更努力而是让机器不重复做已经做过的事。改配置和缓存策略只能算第一步真正拉开差距的是对整个构建链路有没有掌控感。每次点 Run 之前你能预判这次大概多久、哪个 task 会重跑、哪些可以跳过优化才算真正到位。最后再分享一个小技巧每次优化完把最终效果的 profile 报告保存一份。过几个月如果构建又变慢了翻出旧报告对比一下能很快定位是新引入的依赖还是配置回退比重新猜一遍省事得多。
返回列表