第2章 离线依赖缓存机制原理

Gradle 的离线依赖缓存是整个构建系统高速运转的基石。理解缓存目录的组织方式、缓存键的生成逻辑、锁定与校验文件的协作关系,以及命中/未命中的完整路径,才能在设计升级策略时精准决策。本章拆解这四个子模块,并给出可直接用于调试的路径示例与命令。

2.1 缓存目录结构

Gradle 默认将缓存存放在 ~/.gradle/caches/modules-2/files-2.1/ 下。该目录采用 三级嵌套 + SHA-1 子目录 的布局:

  • 一级目录:按 Group (组织/公司) 名称拆分,例如 com.example 会映射为 com/example。
  • 二级目录:Artifact 名称,如 my-lib。
  • 三级目录:版本号,如 1.2.3。
  • 四级目录:由 group:artifact:version:classifier:extension 组合计算出的 32 位 SHA-1 哈希值(小写),作为最终存储文件夹。
  • 文件:原始 JAR/AAR 文件、.sha1 校验文件、.lock 锁定文件。
💡 调试路径示例
~/.gradle/caches/modules-2/files-2.1/
  com.android.tools.build/
    gradle/
      8.5.0/
        e2b9e2a5b3c1d4f6.../   # SHA-1 目录
          gradle-8.5.0.pom
          gradle-8.5.0.pom.sha1
          gradle-8.5.0.pom.lock
          gradle-8.5.0.jar
          gradle-8.5.0.jar.sha1
          gradle-8.5.0.jar.lock

通过此结构,Gradle 可以在毫秒级定位到任意坐标对应的缓存文件,无需遍历全部依赖。

2.2 缓存键生成规则

缓存键的本质是一个 SHA-1 哈希值,生成公式如下:

raw_key = group + ":" + artifact + ":" + version
if (classifier != null)  raw_key += ":" + classifier
if (extension != null)   raw_key += "@" + extension

cache_key = SHA1(raw_key).toLowerCase()   # 32 位十六进制字符串

例如 com.example:my-lib:1.2.3:aar 的哈希计算(伪代码):

SHA1("com.example:my-lib:1.2.3:docs@aar") → “a1b2c3d4e5f6789012345678abcdef0123456789”

这个哈希直接决定了文件在缓存中的存放位置,只要坐标不变,哈希就不变,保证了跨构建/跨项目的一致性。

2.3 锁定文件与校验和文件的作用

文件类型 扩展名 作用
锁定文件 .lock 防止多个 Gradle 进程同时写同一个缓存文件;写进程持有独占锁,读进程等待或跳过
校验和文件 .sha1 存储原始文件的 SHA-1 哈希值,用于验证缓存文件是否损坏或被篡改
元数据文件 .module 等 记录依赖元数据(版本、传递依赖等),同样附带 .sha1 和 .lock
⚠️ 常见问题:当 .lock 文件残留(如进程强制结束)时,新构建会等待数秒至超时后强制清理。手动删除 ~/.gradle/caches/modules-2/ 下的 .lock 文件可快速恢复。

2.4 缓存命中与未命中流程

整个流程可抽象为以下步骤,直接影响构建耗时:

  1. 请求解析:Gradle 根据依赖声明生成缓存键(SHA-1 目录名)。
  2. 检查锁定:尝试获取 .lock 文件的读锁;若被其他进程写锁定,等待或跳过。
  3. 检查文件存在:若目标 JAR/AAR 文件存在,读取其 .sha1 并对比预期哈希:
    ✅ 命中:直接返回文件路径,跳过远程下载。
    ❌ 未命中:进入远程下载流程。
  4. 远程下载并校验:下载至临时文件 → 计算 SHA-1 → 与声明的校验和匹配 → 将文件移动到缓存目录 → 写入 .sha1 → 释放锁定。
  5. 后续构建共享:同一机器的其他 Gradle 进程可直接使用已缓存的依赖。
📊 性能影响
  • 缓存命中:仅 0.1 ~ 0.5 ms(文件 I/O + SHA-1 对比)。
  • 缓存未命中:依赖网络带宽,平均增加 300 ms ~ 30 s 不等。
  • 锁定等待:最多 5 s(默认超时),超过后强制清理并重新下载。
依赖缓存命中与未命中流程 开始:解析依赖坐标 计算缓存键 SHA-1 获取 .lock 读锁 文件存在 & SHA1 匹配? 命中:返回缓存路径 未命中:远程下载 校验 SHA1 → 移动至缓存 写入 .sha1,释放 .lock ⏱ 命中耗时<0.5ms 未命中耗时视网络而定 锁定超时默认 5 秒

2.5 调试与验证命令

以下命令可在构建过程中实时观察缓存行为:

# 观察缓存目录的锁定文件变化(Linux / macOS)
lsof ~/.gradle/caches/modules-2/files-2.1/ | grep .lock

# 强制清除单个依赖的缓存
rm -rf ~/.gradle/caches/modules-2/files-2.1/com.example/my-lib

# 查看缓存命中统计 (Gradle Profiler)
./gradlew --scan
🔍 调试技巧:使用 --refresh-dependencies 参数可强制忽略缓存重新下载,配合 --info 日志可看到每一步的缓存决策日志,例如 Cache entry found for … 或 Cache entry not found, downloading …。

2.6 常见错误与稳定性优化

  • .lock 残留导致构建挂起 → 手动删除 ~/.gradle/caches/modules-2/ 下所有 *.lock 文件,或重启 Gradle 守护进程。
  • 校验和不匹配 → 缓存文件损坏,删除对应 SHA-1 目录后重新下载。
  • 跨用户共享缓存 → 将 ~/.gradle 软链到共享存储时,注意文件权限,否则 .lock 操作可能失败。
  • AGP 升级导致旧缓存失效 → 升级后会自动生成新的缓存键(因为版本号变化),但旧缓存不会自动清理。建议定期执行 gradle clean 并配合 --no-daemon 测试。