ARTICLE DETAIL

资讯详情

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

Gradle依赖缓存corrupt与out of sync排查指南:从定向清理到离线构建

Gradle依赖缓存corrupt与out of sync排查指南:从定向清理到离线构建 很多人看到Gradles dependency cache seems to be corrupt or out of sync这行红字第一反应就是删掉整个.gradle目录重新下依赖。我见过不少同事这么干结果首次构建耗时直接从五分钟变成半小时更离谱的是有时候删完重新构建报错还在。我自己处理过好几次类似问题也在内网环境里踩过Gradle改为本地这个方向的坑。这篇就围绕这个报错把几件事讲透Gradle依赖缓存在磁盘上到底怎么组织、为什么它会损坏和不同步、排查到底要按照什么顺序来、以及真正落地本地化构建时发行版、依赖缓存、仓库地址这三个层面分别该怎么改。另外也会把--refresh-dependencies的真实作用、AGP 与 Gradle 版本矩阵、Windows 下文件锁这些容易被忽略的连锁问题一并说清楚。这篇文章适合 Android 工程构建出问题但不知道从哪下手的开发者也适合负责构建服务器、需要做离线构建环境的人参考。1. 先把报错掰开揉碎Gradle依赖缓存的机制与损坏的本质1.1 依赖缓存到底存在哪、存了什么Gradle 的所有依赖缓存都在同一个根目录下这就是GRADLE_USER_HOME。默认位置在 Windows 上是C:\Users\用户名\.gradleLinux 和 macOS 下是~/.gradle。你可以通过环境变量改它但我强烈建议不要随便改因为很多构建问题一旦叠加自定义路径排查难度直接翻倍。根目录下有四个关键区域分工完全不同路径存放内容容易被误删吗caches/modules-2/files-2.1依赖的实体文件jar/aar 都在这里按 group/module/version 分目录最常被删也是最核心的缓存caches/modules-2/metadata-2.x依赖的元数据比如 POM、模块描述符、版本信息删除时很容易漏掉漏掉会导致 out of synccaches/transforms-3Android AAR 的 Transform 产物经常被忽略删了会重新做 Transform 但不会重新下载wrapper/distsGradle 发行版压缩包和解压后的完整 Gradle 运行时很多人不知道这里删了就要重新下载 Gradle一个典型的依赖在磁盘上的目录结构是这样的~/.gradle/caches/modules-2/files-2.1/com.squareup.okhttp3/okhttp/4.12.0/ /hash/okhttp-4.12.0.jar这里的hash是 Gradle 根据文件名生成的内部哈希同一个依赖的不同文件名会放在不同的哈希子目录下。所以手动找缓存时不要只搜 jar 文件名要按 group 一路找下来。1.2 为什么缓存会 corrupt什么又是 out of syncCorrupt 和 out of sync 其实是两回事但最终都表现为这个报错。Corrupt 是文件本身坏了。比如下载过程中网络中断Gradle 留下了一个不完整的okhttp-4.12.0.jar.part文件但某个异常退出路径下它可能已经被登记进缓存索引。下次构建时 Gradle 认为这个依赖已经缓存过直接引用结果运行时 ClassNotFound 或者干脆报 corrupt。Out of sync 是元数据和实际文件对不上。Gradle 在metadata-2.x里记录了这个模块的某个版本已经下载完成如果你只删了files-2.1下的 jar没有删对应的 descriptorGradle 就会认为文件还在但实际读不到于是报 out of sync。我常用一个类比解释这两者的区别Gradle 像一个信任图书馆管理员的读者图书管理员在登记簿上写下这本书在第三排书架但书其实已经被拿走了读者去找才发现书架空了。corrupt 是书本身缺页out of sync 是登记簿和书架对不上Gradle 没有义务去核对它默认整个图书馆是可信的。1.3 为什么 Gradle 不能自动修复这个缓存这也是很多人困惑的地方既然坏了为什么不自动重新下载因为 Gradle 的缓存设计目标是为了快速复用和离线可用。它在解析依赖时只要发现本地已有完整缓存条目就不会再向仓库发起任何请求。这种设计让第二次构建可以快得离谱但也意味着它不会主动校验已有 jar 的完整性。只有在文件确实缺失、或者解析动态版本需要刷新元数据时它才会重新访问仓库。常见的触发场景包括构建过程中 IDE 被杀进程或者电脑断电。磁盘空间写满导致下载到一半的文件没写完。杀毒软件在后台扫描并锁定.gradle目录下的文件。多个 Gradle daemon 同时操作同一个GRADLE_USER_HOMEWindows 下的文件锁问题尤其突出。手动或同步工具部分覆盖了缓存目录。理解了信任但不验证的机制之后你就会明白出现这个报错时最忌讳的就是乱来。直接删全库不是不行但代价太大而且如果元数据和文件的删除顺序不对反而会制造新的 out of sync。2. 别急着删库跑路一套由轻到重的排查顺序2.1 第一步让日志说话遇到报错我的建议是先关掉 Android Studio 的弹窗打开终端跑一次最简化构建。先跑gradle help或者对 Android 项目跑gradle :app:dependencies --configuration debugRuntimeClasspath这一步的目的不是构建完整 App而是让 Gradle 解析依赖树。如果这个命令也报 cache 相关的错说明问题在全局缓存或 daemon 层面如果它能成功说明崩溃发生在更后面的任务阶段。拿到报错后把--stacktrace --info加上再跑一次gradle build --stacktrace --info重点关注What went wrong下面的描述以及Caused by里指向的具体依赖坐标。这个坐标非常关键它决定你是可以局部修复还是要动整个缓存。2.2 第二步判断是文件坏了还是元数据乱了根据报错定位到具体依赖后先去看对应目录是否存在ls ~/.gradle/caches/modules-2/files-2.1/com.squareup.okhttp3/okhttp/4.12.0/如果目录里没有 jar只有空壳或.part文件那基本可以确定是下载中断留下的残留。这种属于文件缺失处理方式是删除对应目录后重新构建。如果目录里有 jar但构建还是报错那问题很可能出在 metadata 和文件不同步。此时需要连同metadata-2.x里的描述信息一起删rm -rf ~/.gradle/caches/modules-2/files-2.1/com.squareup.okhttp3/okhttp/4.12.0 rm -rf ~/.gradle/caches/modules-2/metadata-2.x/descriptors/com.squareup.okhttp3/okhttp/4.12.0这一步非常容易踩坑。很多资料说删除 modules-2 下的对应目录即可但漏删 descriptors 的结果就是 Gradle 依然认为该版本已缓存构建继续用失效的元数据解析报错依旧。Windows 下没有rm -rf用 PowerShell 执行Remove-Item -Recurse -Force $env:USERPROFILE\.gradle\caches\modules-2\files-2.1\com.squareup.okhttp3\okhttp\4.12.0 Remove-Item -Recurse -Force $env:USERPROFILE\.gradle\caches\modules-2\metadata-2.x\descriptors\com.squareup.okhttp3\okhttp\4.12.02.3 第三步由小到大的修复动作以及--refresh-dependencies的真实作用很多人在这一步会跑gradle build --refresh-dependencies但这真的不是万能药。它的作用是忽略已解析缓存条目重新向仓库确认最新状态。对1.、latest.release、SNAPSHOT这类动态版本它会强制刷新对固定版本号4.12.0只要本地文件还在它基本不会重新下载。所以当你项目里全是固定版本、且缓存文件坏在表面看起来还正常时这个命令跑完往往还是老样子。正确的操作顺序是跑gradle --stop停掉所有 daemon释放文件锁。关闭 Android Studio 或 IDEA因为 IDE 内嵌的 Gradle 可能仍持有锁。按 2.2 的方法做定向删除只删报错坐标对应的目录。重新构建gradle build。如果还是报错把删除范围扩大到整个modules-2目录但保留transforms-3。最后才考虑删除整个caches目录。这里给一个快速判断表报错表现可能原因推荐操作只有某个依赖报错其它构建正常该依赖下载中断或文件损坏删除该坐标目录及其 descriptor多个依赖同时报错且指向仓库解析失败元数据区整体不同步删除modules-2后重建Gradle 一直尝试下载但下载一半失败磁盘空间不足或网络不稳定清理临时文件检查磁盘然后重新构建删除文件时提示被占用Windows 文件锁或 daemon 未退出gradle --stop关闭 IDE必要时重启检查临时文件也很简单扫一眼.gradle根目录下有没有大量.part或.lck文件。.part是未完成下载.lck是残留锁文件。有的话直接删。提示如果是杀毒软件导致的后台扫描锁文件建议把GRADLE_USER_HOME加入杀毒软件白名单。这个操作在很多 Windows 开发机上真的能解决反复无常的 cache 损坏问题。3. 改为本地到底改哪里三个层级一起说清楚标题里的Gradle 改为本地其实不是一个动作而是三个层级的操作。根据你的实际环境可能只需要做其中一层。3.1 层级A把Gradle发行版改成读本地文件could not install gradle distribution from gradle-8.13-bin.zip这类报错是 wrapper 下载发行版失败。Gradle wrapper 会根据项目里的gradle/wrapper/gradle-wrapper.properties去下载指定版本的 Gradle 发行版目标位置是GRADLE_USER_HOME/wrapper/dists。内网环境或下载慢的场景最直接的办法就是改distributionUrl指向本地文件地址。修改前distributionUrlhttps\://services.gradle.org/distributions/gradle-8.13-bin.zip修改后Windows 环境distributionUrlfile\:/D:/gradle-dist/gradle-8.13-bin.zipLinux 环境distributionUrlfile:///opt/gradle/gradle-8.13-bin.zip这里有两个容易翻车的细节。第一如果gradle-wrapper.properties里配置了distributionSha256Sum那么本地 zip 的哈希必须和它一致否则 Gradle 会报 Verification of Gradle distribution failed。如果你手里的 zip 不是官方原版或者不确定哈希干脆把这个字段删掉。第二file:/D:/这种写法里的冒号最好用反斜杠转义因为properties文件解析对冒号敏感不转义在某些环境下会被当作分隔符处理导致 URL 解析异常。这种方式的优点是改动最小缺点是你需要单独维护一个 Gradle 发行版的本地文件仓库。如果你只是一个人开发这个方法最省事。3.2 层级B把依赖缓存整体搬过来另一种改为本地是把整个依赖缓存在机器间迁移。这是我在内网构建机上用得最多的方案。操作步骤在一台可以正常联网、依赖完整的开发机上跑一次gradle build或gradle assembleDebug确保~/.gradle下已经有全部依赖。构建完成后等 daemon 退出把整个GRADLE_USER_HOME打包。注意要包含caches和wrapper两部分否则目标机器还要去下载 Gradle 发行版。拷贝到目标机器解压到对应的用户目录。在目标机器项目根目录的gradle.properties里加上org.gradle.offlinetrueorg.gradle.offlinetrue会让 Gradle 完全走本地缓存任何缺失依赖都会直接报错而不是先尝试联网超时再失败。这一点很关键否则在隔离网络环境下Gradle 每次解析依赖都要等若干秒连接超时体验非常差。如果你只拷贝caches目录也可以但整个.gradle目录一起拷贝更省心。.gradle下还包含gradle.properties、init.d这些配置一起迁过去能保证两端行为一致。这个方案的痛点是增量同步。首次迁移之后如果项目新增了依赖你得回流到有网的开发机上重新拉取一次再把对应新增部分同步过去。维护成本中等胜在实施门槛低不需要搭任何服务。3.3 层级C让仓库地址本地化或镜像化层级B解决的是已有的依赖怎么搬过去但依赖是动态增长的。更稳定、更团队友好的方案是把仓库地址直接改为内网可访问的镜像源。个人项目最简单的做法是在项目的settings.gradle中声明仓库镜像。例如用国内公共镜像pluginManagement { repositories { maven { url https://maven.aliyun.com/repository/gradle-plugin } gradlePluginPortal() google() } } dependencyResolutionManagement { repositories { maven { url https://maven.aliyun.com/repository/public } maven { url https://maven.aliyun.com/repository/google } mavenCentral() } }这里必须特别注意pluginManagement 里的 repositories 和 dependencyResolutionManagement 里的 repositories 是两个体系。plugins {}DSL 找插件是通过 pluginManagement普通依赖是通过 dependencyResolutionManagement。很多人改了依赖仓库但插件还是从官方插件门户下载然后插件下载失败还以为是缓存坏了。团队环境下更正规的做法是在内网搭一个 Nexus 仓库把 Maven Central、Google Maven、Gradle Plugin Portal 都代理一遍然后在所有项目里统一指向内网 Nexus 地址。开发机不需要直连外网依赖解析走内网速度稳定缓存也集中可控。这三种层级看起来复杂实际上就是回答三个问题Gradle 运行时要不要重新下依赖文件要不要重新下新增依赖从哪里下想清楚这三个问题你就知道自己需要做到哪一层。4. 离线化之后的连锁坑版本、插件与IDE4.1 AGP版本与Gradle版本兼容矩阵DSL报错的真相依赖缓存转到本地之后最容易被忽略的是 Gradle 和 Android Gradle PluginAGP的版本匹配关系。很多人把 Gradle 版本随意换到本地能用的某个版本结果项目构建时报出Error: Gradle DSL method not found: minSdkVersion()这类莫名其妙的错误。这个报错十有八九不是缓存问题而是 AGP 和 Gradle 版本严重不匹配。Android Gradle Plugin 的 DSL 方法在解析阶段会动态注册到 Project 上如果 Gradle 版本太老无法识别 AGP 要求的 DSL 语法反过来Gradle 版本太新而 AGP 太老也会出现行为异常。AGP 版本和最低 Gradle 版本的对应关系大致如下AGP 版本最低 Gradle 版本7.07.0.27.27.3.37.47.58.08.08.28.2具体对应关系以 Android 官方文档为准但核心原则是离线环境下更要保持版本矩阵一致。用旧的 AGP 搭配过新的 Gradle或者反过来都有可能在配置阶段报出难以理解的 DSL 错误。另外有个容易混淆的场景Flutter 项目早期是使用apply plugin:命令式方式应用 Flutter 的 Gradle 插件新版要求改用plugins {}声明式方式。如果项目里配置混乱构建会报出You are applying Flutters main Gradle plugin imperatively using the apply这样的提示。这类问题同样会被误判为缓存损坏实际要做的是对齐官网推荐的配置结构而不是动缓存目录。4.2 动态版本与SNAPSHOT在离线模式下的表现离线模式下有一个隐藏很深的坑动态版本和 SNAPSHOT 版本的解析行为。1.、latest.release、2.0-SNAPSHOT这类坐标在离线模式下 Gradle 不会重新检查仓库它只认本地缓存里已有的解析结果。如果你的项目在联网环境没有提前解析过最新动态版本切到离线环境后即使缓存里有更新的文件Gradle 也可能使用旧的解析结果甚至直接报找不到模块。所以凡是需要进入离线环境的项目我强烈建议把所有动态版本锁定为固定版本号。这是最容易被人忽略导致离线后构建行为和联网时不一致的原因。4.3 Android 的 transforms-3 与 IDE 层面的坑Android 项目在依赖缓存之外还有一层 Transform 缓存位于caches/transforms-3。它存的是 AAR 处理后的产物比如压缩资源、类重打包之后的结果。很多人把modules-2删了重建但transforms-3还保留着旧内容构建时 AGP 发现 Transform 产物的元数据和当前 AAR 版本对不上又会报新的错误。处理方式很简单如果已经重建了modules-2顺手把transforms-3一起删掉。这个目录删除后不会触发重新下载只会让 Android 构建在 Transform 阶段多花几分钟属于可接受的代价。IDE 层面也有一个经典坑。Android Studio 的 Gradle 工具窗口和命令行用的其实是同一个GRADLE_USER_HOME但 IDE 会持有独立的 daemon。你在命令行里清了缓存回到 Android Studio 点 Sync报错还在多半是 IDE 的 daemon 还锁着旧文件。正确做法是在命令行执行gradle --stop然后在 AS 里停止所有 Gradle 任务进程必要时重启 IDE。提示如果项目在 Android Studio 的Settings - Build Tools - Gradle里选择了 Use local Gradle distribution请确认本地 Gradle 路径对应的版本和项目里 wrapper 要求的版本一致。IDE 的这个设置很容易变成本地版本不匹配的来源和缓存报错叠加在一起就会非常迷惑。5. 让它长期别坏几件值得养成的小事5.1 固定GRADLE_USER_HOME并统一gradle.properties构建环境最怕这个项目一套配置、那个项目另一套配置。我见过有人图省事在每个项目的gradle.properties里写不同的堆内存参数还有人把GRADLE_USER_HOME指向共享网络盘结果并发构建互相踩缓存。我的建议是开发机保持默认的~/.gradle目录不要设置全局环境变量去改它。构建服务器可以明确指定一个统一路径但所有项目共用同一套gradle.properties基础配置org.gradle.jvmargs-Xmx4g -XX:MaxMetaspaceSize1g org.gradle.paralleltrue org.gradle.cachingtrue org.gradle.offlinetrue这里org.gradle.cachingtrue开启的是构建缓存和依赖缓存是两个概念。构建缓存缓存的是任务输出能让你在干净工作区下复现之前的编译结果对提升构建稳定性很有帮助。5.2 用脚本定期清理过期缓存Gradle 的依赖缓存没有自带过期自动清理机制这是很多人不知道的。时间久了modules-2下会堆满大量历史版本的 jar几百 MB 甚至几个 GB 都很常见。磁盘一紧张下载新依赖时就容易写入失败最后演变成 cache corrupt。可以用一个简单的脚本定期清理临时文件和过期产物。Linux/macOSfind $HOME/.gradle -name *.part -delete find $HOME/.gradle -name *.lck -delete find $HOME/.gradle/caches -type d -name *-tmp -exec rm -rf {} 2/dev/nullWindows PowerShellGet-ChildItem $env:USERPROFILE\.gradle -Recurse -Include *.part, *.lck -File | Remove-Item -Force这个脚本只清理残留和临时目录不会误伤正常依赖。至于真正删除未使用的大版本依赖我建议谨慎操作宁可磁盘多占一点也别让下次构建重新下载半天。5.3 最后一条经验把排查顺序沉淀成习惯处理 cache 报错多了之后我自己养成了一套固定习惯先看日志确认具体依赖坐标再开文件管理器看对应目录是否存在然后定向删除最后才考虑动整库。整个流程通常五分钟内能定位问题比删库重下快得多。这里多提一句不要把 cache corrupt 当成只能靠删除解决的问题。很多情况下它只是下载中断残留、杀毒软件锁文件、或者 AGP/Gradle 版本不匹配的伪装。真正定位了根因解决成本往往很低。如果你现在正被这个报错折磨按我上面第 2 节的顺序走一遍大概率能省下大半天的时间。后续打算做内网或隔离环境的本地构建再对照第 3 节的三个层级选一个适合自己的方案比直接照搬网上的删除 .gradle 目录教程要靠谱得多。
返回列表