1. OkHttp缓存基础与架构概览
OkHttp 的缓存机制是其高效网络通信的核心支柱之一。在排查缓存相关问题之前,必须理解其底层架构:缓存是如何通过拦截器链、磁盘存储结构和策略枚举协同工作的。本章从“现象识别”出发,还原一个典型缓存未命中问题的排查过程,逐一拆解这些基础组件。
现象识别
应用中的图片列表在弱网环境下频繁加载旧数据,用户下拉刷新后仍显示数分钟前的封面。日志中观察到以下特征:
- HTTP 响应头中
Cache-Control: max-age=60存在,但 OkHttp 未命中缓存; - 每次请求都触发了完整的网络往返;
- 磁盘缓存目录无新文件写入。
数据采集
第一步:开启 OkHttp 事件监听器,捕获缓存决策日志:
OkHttpClient client = new OkHttpClient.Builder()
.eventListener(new EventListener() {
@Override
public void cacheHit(Call call, Response response) {
System.out.println("Cache HIT: " + call.request().url());
}
@Override
public void cacheMiss(Call call) {
System.out.println("Cache MISS: " + call.request().url());
}
})
.cache(new Cache(new File("cache_dir"), 10 * 1024 * 1024))
.build();
输出:
Cache MISS: https://example.com/api/images
Cache MISS: https://example.com/api/images
Cache MISS: https://example.com/api/images
第二步:检查磁盘缓存目录结构,发现仅存在 journal 文件,无实际缓存条目:
$ ls -la cache_dir/
total 12
drwxr-xr-x 2 user group 4096 Apr 1 10:00 ./
drwxr-xr-x 3 user group 4096 Apr 1 10:00 ../
-rw-r--r-- 1 user group 134 Apr 1 10:00 journal
原因定位
结合日志与目录状态,进入判断流程:
故障树:缓存 Miss 判断规则
- 是否有
Cache实例注入客户端? 是 → 进入下一步。 - 响应是否包含
Cache-Control或Expires? 是 → 进入下一步。 - 请求方法是否为
GET? 是 → 理论上应可缓存。 - 磁盘
journal是否显示清理错误? 是 → 查看 journal 内容。
查看 journal 文件:
libcore.io.DiskLruCache
1
1
DIRTY 1234
REMOVE 1234
CLEAN 1234 256
READ 1234
REMOVE 1234
频繁的 REMOVE 记录表明缓存条目被快速删除。定位到 DiskLruCache 的清理策略:当条目大小或数量超过阈值时,LRU 淘汰会立即发生。此处是因为并发写入导致 journal 损坏后的自动重建,清空了所有条目。
修复动作
| 组件 | 问题 | 修复 |
|---|---|---|
| DiskLruCache | journal 文件频繁 rewrite,导致缓存落地失败 | 调整缓存目录路径,避免多进程共享;升级 OkHttp 至 4.11+ |
| CacheStrategy | 服务端返回的 max-age=60 被客户端 Cache-Control: no-cache 覆盖 |
检查请求拦截器中是否存在强制 no-cache 逻辑 |
在应用拦截器中移除了错误注入的 Cache-Control 头,并修复了多进程访问同一缓存目录的问题:
// 错误用例:强制不缓存
return chain.proceed(request.newBuilder()
.header("Cache-Control", "no-cache")
.build());
// 修复后:移除强制头,让 OkHttp 根据响应自动决策
return chain.proceed(request);
复测
修复后再次采集日志:
Cache HIT: https://example.com/api/images
Cache HIT: https://example.com/api/images
同时磁盘目录中出现新的条目文件:
$ ls cache_dir/
journal 1234.0 1234.1 5678.0 5678.1
预防
为防止类似缓存行为异常,建立以下检查清单:
- 规则 1: 每次部署前执行
CacheTester单元测试,验证cacheHit和cacheMiss事件触发符合预期。 - 规则 2: 监控
DiskLruCache的journal大小,超过 1MB 时触发告警。 - 规则 3: 在 CI 中集成 OkHttp 的
mockwebserver,模拟Cache-Control场景进行回归。
常见陷阱: 部分开发者在
Interceptor 中错误地调用了 response.cacheResponse() 并尝试修改,这会导致缓存条目被标记为 DIRTY 后立即被 REMOVE。应始终通过 CacheStrategy 控制缓存行为,而非直接操作响应对象。
核心组件对照表
| 组件 | 职责 | 排查要点 |
|---|---|---|
| 缓存拦截器 (CacheInterceptor) | 决定是否从缓存取或发网络请求 | 检查 CacheStrategy 是否返回 network 或 cache 策略 |
| DiskLruCache | 以 journal 文件 + 条目文件形式持久化缓存 | journal 大小、DIRTY/REMOVE 比例、条目数 |
| CacheStrategy | 封装 RFC 7234 缓存策略:时间、验证、约束 | 是否启用了 only-if-cached 或 max-stale |
排查速查表: 当缓存异常时,首先检查
EventListener.cacheMiss 是否被连续触发。若为是,下一步查看 DiskLruCache.journal 的 CLEAN 记录数量——若为零,则表明缓存从未成功落地。此时应聚焦在写入路径的安全退出(Snapshot 未关闭)或目录权限问题。