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 判断规则
  1. 是否有 Cache 实例注入客户端? 是 → 进入下一步。
  2. 响应是否包含 Cache-Control 或 Expires? 是 → 进入下一步。
  3. 请求方法是否为 GET? 是 → 理论上应可缓存。
  4. 磁盘 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 控制缓存行为,而非直接操作响应对象。
OkHttp缓存架构核心节点与数据流 应用拦截器 缓存拦截器 网络拦截器 请求/响应头注入 CacheStrategy 决策:命中/未命中/条件 网络请求 DiskLruCache journal 文件 条目条目: key.0 条目条目: key.1 LRU 淘汰 清理线程 CacheStrategy 枚举 FORCE_CACHE · SKIP_CACHE · STORE_ON_DISK · 条件请求(If-Modified-Since / If-None-Match) 缓存命中返回 / 未命中继续 写入

核心组件对照表

组件 职责 排查要点
缓存拦截器 (CacheInterceptor) 决定是否从缓存取或发网络请求 检查 CacheStrategy 是否返回 network 或 cache 策略
DiskLruCache 以 journal 文件 + 条目文件形式持久化缓存 journal 大小、DIRTY/REMOVE 比例、条目数
CacheStrategy 封装 RFC 7234 缓存策略:时间、验证、约束 是否启用了 only-if-cached 或 max-stale
排查速查表: 当缓存异常时,首先检查 EventListener.cacheMiss 是否被连续触发。若为是,下一步查看 DiskLruCache.journal 的 CLEAN 记录数量——若为零,则表明缓存从未成功落地。此时应聚焦在写入路径的安全退出(Snapshot 未关闭)或目录权限问题。