ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android构建优化实战:从10分钟到10秒的Gradle/KAPT调优全记录

Android构建优化实战:从10分钟到10秒的Gradle/KAPT调优全记录 你是不是也遇到过这种场景改了一行布局代码起身接了杯水回来发现Android Studio还在转圈。看了眼状态栏Gradle的进度条慢悠悠地爬着心里默默数了一下又是好几分钟。项目越做越大模块越拆越多构建时间从最初的1分钟变成3分钟再到10分钟最后你甚至养成了“改完代码先去上厕所”的习惯。这篇文章不谈那些高大上的理论只聊我实实在在踩过的路。把一个编译速度从10分钟拖到的Android项目一步步优化到增量构建10秒左右完成的全过程。里面涉及Gradle配置、Kotlin/KAPT瓶颈、资源处理、构建缓存这几个核心方向也包含了很多只有真正被慢构建折磨过的人才会注意到的细节。适合受够了慢构建的Android开发、想给老项目动刀但不知道怎么下手的同学参考。顺便说一句这个项目后来还接了一些ARouter、Room这类依赖注解处理的框架所以KAPT优化那部分对你同样适用。1. 十分钟到底耗在哪先给项目做个体检任何优化都不能拍脑袋第一步永远是搞清楚时间花在了哪里。我当时的第一反应是“项目代码太多了”但真去分析之后发现根本不是这么回事。Gradle构建时间通常分成两个阶段Configuration配置阶段和Execution执行阶段。前者负责读脚本、建任务图后者才是真正干活的编译、打包环节。很多老项目的问题恰恰是配置阶段异常臃肿几十个模块的脚本要逐个执行光读build.gradle就要花掉两三分钟。要定位具体的耗时千万别靠猜直接用工具。Android Studio自带的Build Analyzer就很好用在Build窗口里点一下就出来了。它能把每次构建的Critical Path列出来清楚看到哪个Task最耗时。命令行党可以用这招./gradlew assembleDebug --profile --rerun-tasks执行完以后项目根目录build/reports/profile目录下会生成一个HTML报告打开就能看到每个Task的执行时间、每个项目的配置耗时非常直观。我当时拿到的数据是这样的全量Clean构建10分钟其中Configuration占掉了1分半KAPT注解处理占掉了将近3分钟Dex打包占掉1分半AAPT2资源处理和压缩再吃掉1分多钟。看到这个分布优化的方向一下就清楚了。这里有个很重要的经验优化前先做减法。debug构建里有些任务压根没必要在这个阶段跑比如lint、test、单元测试、资源压缩和混淆。这些在release构建里该开的开但日常开发时要学会把它们关掉让每一秒都花在刀刃上。2. gradle.properties几行配置换分钟级体验很多人拿到项目第一件事就是改gradle.properties但改完发现没什么效果。原因往往是不理解每行配置的含义只是机械地堆参数。我这里直接给出一份实测有效的完整配置再拆开解释为什么这么写org.gradle.jvmargs-Xmx8g -XX:MaxMetaspaceSize1g -XX:HeapDumpOnOutOfMemoryError -Dfile.encodingUTF-8 org.gradle.paralleltrue org.gradle.cachingtrue org.gradle.daemontrue org.gradle.workers.max8 kotlin.daemon.jvmargs-Xmx4g kotlin.incrementaltrue kotlin.incremental.javatrue android.useAndroidXtrue android.enableJetifierfalse org.gradle.configureondemandtrue这里面的门道我逐条说一下。内存参数是地基。-Xmx8g是给Gradle主进程的最大堆内存。你可能会想“给越大越好”但实际并不是。堆太大GC反而会更频繁地做Full GC堆太小大型项目直接OOM。8G这个值对大多数中大型项目够用。如果你的开发机是16G内存这个值还要根据模拟器、Android Studio本身占用的内存做权衡别一下子全给Gradle。Metaspace是JVM存放类元数据的地方。项目模块多、依赖多的时候Metaspace很容易涨到几百M甚至1G。默认值不够就会出现频繁的类加载和回收。我见过一个项目把-XX:MaxMetaspaceSize调成512m之后配置阶段直接从一分半钟降到了四十秒。parallel和workers.max一定要配合着看。org.gradle.paralleltrue是让多个模块并行构建前提是你拆分了模块workers.max8是控制最多同时跑几个worker。这个值建议设成CPU核心数-1留一个核给主线程处理调度。设太满反而会因为线程切换损耗性能。caching和daemon是两个被严重低估的配置。org.gradle.cachingtrue开启的是build cache它能把Task级甚至更细粒度的输出缓存下来同一份输入不重复跑任务。daemon则是Gradle后台守护进程避免每次构建重新启动JVM。这两项叠加增量构建的体验会有一个质的提升。特别说明一下daemon默认就是开启的我把它写出来只是为了提醒你确认一下没被项目里的配置覆盖掉。configureondemand是一把双刃剑。它只配置需要的模块确实能缩短配置阶段但在一些依赖关系不清晰的老项目里它可能导致任务图构建不完整出现“改了A模块但B模块没编译”这种诡异问题。我的建议是只在项目结构清晰、模块依赖明确的情况下开启否则你省下的时间都会在排查诡异的构建问题中加倍赔回去。还有一个坑必须提醒android.enableJetifier。如果项目里引入的第三方库还在用老的support库依赖把它设为true会触发全局的依赖改写每次构建都要扫描所有依赖耗时非常明显。我的做法是把所有老库全部升级到AndroidX版本然后把这项直接关掉。如果实在有库升级不了再考虑开着但要清楚它带来的代价。3. Kotlin编译链路的真正瓶颈KAPT和守护进程等比例看完Gradle配置第二阶段是把Kotlin编译这条链路单独拎出来。前面说了实测数据里KAPT占了将近3分钟是全项目最耗时的一个Task没有任何一个其他任务能和它抗衡。这里面的原因值得掰开揉碎了讲。KAPT的工作原理是先把Kotlin源码编译成Java的stub桩代码然后再让Java的注解处理器跑在这些stub上。等于Kotlin编译一次Java编译一次中间还多出一层stub生成。这就是为什么任何使用注解处理器比如Room、ARouter、GlideAPT、EventBus的项目构建时间都会肉眼可见地膨胀。你写的业务代码本身没多少光是生成和解析这些stub就消耗了大量时间。当时我把项目里使用的Room、ARouter、Glide这些库的注解处理器列了一个清单查了一圈发现它们都有对应的KSP版本。KSPKotlin Symbol Processing API直接基于Kotlin编译器解析符号不需要生成Java stub那一步处理注解的速度比KAPT快了好几倍。迁移的步骤其实没那么吓人plugins { id(com.google.devtools.ksp) version 1.9.20-1.0.14 }然后把kapt(androidx.room:room-compiler:2.6.0)替换为ksp(androidx.room:room-compiler:2.6.0)把kapt关键字全局替换成ksp就行。注意检查一下用到kapt { arguments {} }的写法KSP对应的参数传递方式是ksp { arg(room.generateKotlin, true) // 更开放的参数配置 }这一步做完实测效果惊人光Room的编译处理就从原来的70秒掉到10秒出头。如果你用的框架暂时还没有KSP支持那就只能尽量提高KAPT的工作效率kapt.use.worker.apitrue kapt.incremental.apttrue这两行能让KAPT在增量模式下跑并且支持并行worker虽然不是质的飞跃但也能压掉不少时间。另一个容易忽略的是Kotlin守护进程的独立内存。Kotlin编译不会走Gradle的JVM而是自己起一个kotlin daemon进程。如果这个小进程内存不够编译期间就会反复GC表现就是CPU一直跑着但编译进度卡住不动。单独给它分4G内存非常重要kotlin.daemon.jvmargs-Xmx4g另外如果你用的是1.8以下的Kotlin版本强烈建议升级到新版本。Kotlin 1.9.20之后K2编译器已经默认开启了编译性能比旧的前端编译器提升了一个档次。我当时顺手把Kotlin从1.7升到1.9同一段代码全量编译时间又缩短了差不多20%。这个提升属于白捡的只要注意一下项目里其他组件的Kotlin版本兼容性基本不会有什么大问题。4. AAPT2与资源处理debug构建里藏着的耗时大户很多人把Gradle配置翻了个底朝天却忽略了一个藏在深处的耗时环节资源处理。AAPT2是Android官方的资源编译器它负责编译所有res目录下的资源文件生成资源表。项目里的图标、图片、切图一多光是资源编译这一步就能吃掉一分多钟在老项目里甚至更夸张。先说最基本的开关。debug构建里有两个资源相关的操作完全可以关掉android { buildTypes { debug { shrinkResources false minifyEnabled false } } }minifyEnabled true触发的是代码混淆和裁剪shrinkResources true则是在此基础上裁剪无用资源。这两项在release构建里是必须的但在debug里开着纯属浪费——你调试的时候根本不需要混淆每跑一次构建还要额外做一轮资源压缩极其低效。接着是resConfigs。一个正经项目通常都会引一堆第三方依赖库这些库附带了几十种语言的翻译资源。Gradle打包的时候会把这些资源全部塞进来AAPT2处理的时间也水涨船高。但你实际需要的可能就中文和英文两个语言android { defaultConfig { resConfigs zh, en } }配置完之后AAPT2编译三方的多语言资源时可以丢弃不需要的语种目录资源量直接砍掉一大半。这个配置的收益相当见效尤其在依赖了Play Services那套库的时候。还有一类问题是很多人没意识到的体积巨大的raw资源。有人喜欢把视频、大体积音频文件放在res/raw目录下。AAPT2在处理raw目录时不会像普通drawable那样压缩但它会参与资源链接索引一个大文件就会让链接阶段卡很久。我的建议是这些大文件全部挪到assets目录它们在里面是原样打入APK的不需要经过AAPT2的资源索引流程。再提一个矢量图相关的点。老项目里经常有成百上千张PNG切图每张图AAPT2都要做一遍crunch处理。如果你用Android Studio的Vector Assets把纯色图标转换成矢量图这些图标就完全不需要AAPT2做位图压缩处理了。能转的尽量转能删的删资源处理这部分的时间会肉眼可见地降下去。5. 增量构建与缓存从“勉强快”到“秒级编译”的分水岭前面几轮优化做完我的全量Clean构建已经从10分钟降到了2分半左右日常增量构建也稳定在1分钟上下。但说句实话这个速度依然配不上“秒级”这个词。真正让增量编译来到10秒级别的是构建缓存和增量机制的深度调优。先理解一下增量构建的原理Gradle在执行每个Task时都会对比输入和输出。输入没变任务直接跳过输出直接从缓存里拿。这就是所谓的UP-TO-DATE机制。但很多不起眼的小问题会破坏这个机制让每次构建都退化成全量构建。最常见的元凶是BuildConfig的动态生成。很多人习惯在build.gradle里这么写buildConfigField(String, BUILD_TIME, \${System.currentTimeMillis()}\)这行代码的本意是给BuildConfig写入时间戳结果就是每次构建BuildConfig.java的输入都变了依赖它的所有模块全部重新编译。一次两次看不出来日积月累每次构建都要付出全量编译的代价。我当时把它改成了从gradle.properties读取一个固定的构建序号只在release打包时更新dev构建不再动态生成增量构建一下就丝滑了。破坏增量的第二个常见因素是动态生成的资源文件。有些插件会在preBuild阶段修改或生成res文件——比如动态替换图标、根据环境修改strings.xml。这些操作相当于直接污染了资源Task的输入AAPT2只能重新跑一遍。如果插件允许尽量把这些动态生成逻辑从preBuild挪到单独的Task并且只在执行特定任务时才触发。第三个是配置缓存Configuration Cache。在Gradle 7和AGP 7.0之后org.gradle.configuration-cachetrue已经可以用得很好。它能把Configuration阶段的处理结果缓存下来第二次构建直接跳过整个配置阶段。我这边的实测是配置阶段从1分半压到10秒内。这个优化对老项目尤其给力因为老项目通常脚本复杂、配置阶段长。Build Cache本身也要正确理解。org.gradle.cachingtrue开启的是Task输出缓存缓存位置默认在~/.gradle/caches/build-cache-1目录。它的价值在于同一个任务只要输入没变即使你执行了clean或者在其他分支上构建也能直接从缓存恢复输出不用重新算。这对CI分发和分支切换场景特别重要。有条件的话可以配远程缓存服务器但单机开发的话本地缓存已经能带来不错的收益了。还有一点提醒一下不要随手点Clean Project。很多人跑构建前习惯性点clean手一滑就是15分钟的全量重编。我在团队里明确要求过除非换了SDK版本、改了Gradle配置、资源构建逻辑变化否则一律不准clean。这个习惯改过来之后团队的构建速度整体都上了一个台阶。6. 实测数据每一步优化对应的耗时变化空口无凭我把自己项目里每一步操作前后的实测数据列出来方便你对照参考。测试环境是MacBook Pro M1 Pro16G内存项目包含11个模块Kotlin为主Java大概占了四分之一的代码量。构建命令是./gradlew clean assembleDebug全量和./gradlew assembleDebug增量。因为M1的性能本身就不错全量最终的2分半在Intel机器上可能要翻倍请注意理性对照。优化项Clean全量构建增量构建改一行代码优化前AGP 4.2 Kotlin 1.4 KAPT 无缓存无并行10分12秒4分30秒开启parallel 调大JVM内存 build cache7分05秒2分40秒移除debug下lint/shrink/minify等冗余任务6分10秒2分20秒resConfigs语言限制 大资源移出res5分40秒2分05秒KAPT全量切换为KSP3分20秒1分15秒Kotlin升级到1.9K2编译器2分40秒50秒开启configuration-cache 修掉BuildConfig动态时间戳2分35秒12秒这个表格读出来可能有人不信增量构建凭什么能从50秒降到12秒关键在于修复BuildConfig动态时间戳加上配置缓存跳过Configuration阶段后改一行Java代码只需要重新编译对应的模块、再走一遍打包和Dex。而这个项目模块切分得足够细被改动的模块往往只是一个叶子模块编译它本身就是几秒钟的事情。如果你的项目大到模块几十个org.gradle.paralleltrue和workers.max的调优收益会更大。把模块拆分成“底层公共组件模块 业务组件模块 壳工程”三层结构让大多数地方修改只落在叶子模块上秒级编译完全是可以做到的。这里也顺带回答一个常见疑问“全量构建能不能到10秒”我的答案是正常的多模块App项目很难完全Clean后要重编所有模块和依赖10秒这个数字在业界的表述里更多指的是“增量构建”或“热部署后的构建”。如果有人宣称全量Clean也能10秒那他要么项目小得没什么代码要么就是开了远程缓存配好了缓存前端效果和没重编差不多。7. 优化之外那些配置解决不了的问题构建速度这件事配置只是上半场下半场拼的是设备和仓库卫生。我有几个踩过的坑想单独整一个部分写出来因为它们不在任何官方文档的优化清单里却实实在在地拖慢过我的项目。第一个坑杀毒软件和文件索引服务。Windows上开发的同学注意了Windows Defender默认会实时扫描Gradle的临时目录、build目录和daemon进程访问的文件。如果你排查完所有配置项依然找不到构建慢的原因可以把项目目录和Gradle缓存目录~/.gradle加入Defender的排除列表实测构建时间能缩短20%-30%。macOS用户也一样Spotlight索引如果反复扫描build目录和.git目录同样会造成文件监听异常和I/O波动。第二个坑项目目录的同步工具。我之前有个同事把项目目录放在OneDrive同步网盘里这个决定直接让每次构建时间翻倍而且不是慢一点点。这类同步工具会持续监听文件变化、上传下载Gradle创建task时还在频繁读写文件两者互相干扰。Gradle官方对这种情况也没有好的解法最省事的方案就是把项目放到本机磁盘的纯本地路径下不要放在任何云端同步目录里至关重要。第三个坑AGP版本和Gradle版本不匹配。每次升级Gradle wrapper和AGP不是随随便便的事但它们之间的对应关系对构建性能影响很大。随便举几个例子AGP 7.x搭配Gradle 7.x是稳定的AGP 8.x则要求Gradle 8.x起步。老组合的默认参数可能带着历史包袱比如更保守的资源缓存策略、更低的worker上限。升级一次AGP光靠新版默认配置就能省出不少时间。这里需要注意升级后跑一次clean并重新检查废弃配置项把旧的配置迁移完。第四个坑依赖传递地狱。模块数量少但依赖很深的时候每次编译的耗时可能比模块多但依赖扁平的项目更严重。我接手过一个项目A模块依赖BB依赖CC依赖D每次改A的代码Gradle也要把B、C、D的编译结果拿过来检查变化一旦中间某个模块的BuildConfig字段变化整条链路全部重编。后来我做了依赖瘦身和模块合并把垂直依赖改成扁平化结构能直接用implementation的地方不用api该移到公共模块的公共代码尽早移出去。这个举措对增量的改善特别真实因为Gradle的Task依赖图越扁平计算量越小。至于设备本身有条件换NVMe固态硬盘比机械硬盘快好几倍。开发机内存建议32G起步让Gradle、Kotlin daemon、Android Studio、模拟器四者都活得舒服。内存不够时系统会把大量热点文件放到交换区那是一种比慢更折磨人的体验——CPU和I/O交替冲高构建进度以像素级别蠕动检查什么配置都没用。还有个小提醒如果你的项目启用了热修复、插件化这些动态加载方案它们的编译插件通常会在构建链路上加很多自定义Task这些Task往往会破坏Gradle原生的增量检查。我理解这类业务需求没法砍但可以尽量在使用这些插件和享受快速构建之间做一个隔离方案比如把插件化的动态模块单独抽离日常开发不参与主流程构建只在需要发版时再整体构建。这样既保住了业务方案又不拖垮日常开发效率。最后聊一个团队层面的体会构建优化只有你一个人知道怎么做是没用的它必须变成一条约定。我在团队里最常做的一件事就是打印一份“保持增量”的清单给每个人——不BuildConfig写时间戳、不随手clean、改动尽量收敛在叶子模块、新增依赖用implementation而不用api。每次有人违反构建耗时的警告排名就会重新爬上排行榜这时候不用我多说什么当事人自己就会去改掉。Gradle的构建日志和Build Analyzer一直在替大家体检把构建时间当作一件需要持续维护的指标不要等它慢到无法忍受才想起来动手。
返回列表