缓存写入流程与存储结构:响应写入 DiskLruCache,元数据管理,文件命名与清理
OkHttp 的离线缓存模块并没有自行实现磁盘存储的底层细节,而是直接委托给 Google 的 DiskLruCache 组件。一个 HTTP 缓存条目在磁盘上被拆解为 元数据(.0 文件) 和 响应体(.1 文件) 两部分,通过原子性的写入流程保证数据一致性。本章深入分析响应从网络进入磁盘的完整管道。
核心设计原则: 写入即原子提交。只有在所有数据(元数据 + 主体)都成功写入后,
commit() 才会将条目标记为可用;否则通过 abort() 回滚,避免产生残缺的缓存文件。
写入流程深度解析
当 CacheInterceptor 决定缓存一个来自网络的响应时(状态码 200、GET 请求、非 no-store),它会将数据和元数据提交给 Cache.put() 方法,该方法进一步委托给底层的 DiskLruCache。
- 获取编辑器: 调用
diskLruCache.edit(urlMd5)获取一个Editor实例。如果正在执行并发写入,该方法返回 null,当前线程放弃缓存。 - 写入元数据: 通过
Editor.newSink(0)打开索引为 0 的输出流,写入请求行、响应头、Vary 头、TLS 握手信息以及时间戳。 - 写入主体: 通过
Editor.newSink(1)打开索引为 1 的输出流,逐块写入响应体的原始字节。 - 提交或中止: 所有数据写入成功后调用
Editor.commit()原子性提交;若中途发生异常,调用Editor.abort()删除已写入的临时文件。
调试命令: 运行时可以通过以下 ADB 命令查看缓存目录的文件分布与大小:
adb shell ls -la /data/data/{your_package}/cache/okhttp_cache/
# 输出示例:
# -rw-rw---- 1 u0_a123 u0_a123 24576 Mar 10 10:00 journal
# -rw-rw---- 1 u0_a123 u0_a123 2048 Mar 10 10:01 5d41402abc4b2a76b9719d911017c592.0
# -rw-rw---- 1 u0_a123 u0_a123 15248 Mar 10 10:01 5d41402abc4b2a76b9719d911017c592.1
磁盘文件命名与存储结构
OkHttp 的 DiskLruCache 在日志文件(journal)中维护索引,而实际数据则分离到不同的磁盘文件中。每个缓存条目对应两个数据文件,文件名由 URL 的 MD5 哈希值生成。
| 文件路径 | 内容 | 生命周期 |
|---|---|---|
journal | 操作日志文件,记录所有读写、提交、回滚操作。用于重启后重建 LRU 顺序。 | 贯穿缓存实例的整个生命周期 |
{url_hash}.0 | 元数据文件:包含请求行、响应头、Cache-Control 指令、Vary 头、TLS 连接信息。 | 随条目被清理而删除 |
{url_hash}.1 | 主体文件:存储 HTTP 响应体的原始二进制数据。 | 随条目被清理而删除 |
元数据管理:.0 文件内部结构
元数据文件是决定缓存命中与新鲜度校验的核心。OkHttp 按照固定格式将结构化的键值对写入 .0 文件,使得无需读取 .1 文件即可完成缓存决策。
http://example.com/data
GET
0
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: public, max-age=3600
Vary: Accept-Encoding
OkHttp-Received-Millis: 1715360000000
OkHttp-Sent-Millis: 1715360001000
其中 OkHttp-Received-Millis 和 OkHttp-Sent-Millis 是 OkHttp 内部注入的自定义头,用于参与新鲜度计算和条件请求(If-Modified-Since / If-None-Match)的判断。
清理策略与 LRU 淘汰机制
当写入操作导致缓存目录的总大小超过构造时设置的 maxSize,DiskLruCache 会在后台线程中调用 trimToSize() 方法进行清理。清理按照 LRU(最近最少使用) 顺序进行,从 journal 文件中读取访问时间戳,逐条淘汰最久未被读取的条目,直到总容量回落到安全阈值以下。
常见错误:FileNotFoundException
在高并发写入或空间紧张时,一个线程正在读取的缓存文件可能被另一个线程的 LRU 清理删除。此时 OkHttp 会抛出
在高并发写入或空间紧张时,一个线程正在读取的缓存文件可能被另一个线程的 LRU 清理删除。此时 OkHttp 会抛出
FileNotFoundException 或 IOException。应用层的 CacheInterceptor 会捕获此类异常并自动降级为网络请求,开发者在实现自定义缓存策略时也应该遵守这一原则:读取缓存时始终准备好回退到网络。
性能与稳定性优化:
- 原子提交: 始终使用
commit()/abort()配对,避免产生“部分写入”的脏数据。 - 控制 journal 文件大小: journal 过大时会触发重组(rebuild),此时 I/O 压力较大。应合理设置
maxSize,避免频繁触发清理阈值。 - 监控缓存命中率: 通过
Cache.hitCount()和Cache.networkCount()监控写入是否生效,及时调整缓存配置。