第2章 缓存拦截器与责任链机制

OkHttp 的缓存能力并非孤立的模块,而是通过 责任链(Chain of Responsibility)设计模式,以拦截器链的方式织入请求-响应全流程。CacheInterceptor 正是这条链上负责缓存读写、校验、更新的核心节点。理解它在链中的位置与传递流程,是掌握整个缓存机制的第一步。

本章目标
✓ 掌握 OkHttp 拦截器链的整体拓扑
✓ 理解 CacheInterceptor 在链中的上下游关系
✓ 厘清请求/响应在缓存节点上的完整传递顺序
✓ 掌握责任链模式下各拦截器的职责隔离

2.1 OkHttp 拦截器链概览

OkHttp 将一次网络请求拆解为多个拦截器(Interceptor),每个拦截器只处理自己关心的逻辑,然后通过 chain.proceed() 将请求传递给下一个拦截器,最终获得响应后再逐层返回。这种管道式架构使得缓存、重定向、重试、连接复用、数据转换等能力可以被灵活组合。

拦截器类 职责 执行顺序
RetryAndFollowUpInterceptor 失败重试、重定向跟踪 第1个(应用层首入)
BridgeInterceptor 请求头转换(Cookie、Gzip、Content-Length) 第2个
CacheInterceptor 缓存读取、条件更新、缓存写入 第3个
ConnectInterceptor 建立 TCP/TLS 连接 第4个
CallServerInterceptor 发送 HTTP 请求并读取原始响应 第5个(最内层)
关键约定:拦截器链的“内外”概念——越靠前的拦截器越“外层”,越靠后的拦截器越“内层”。CacheInterceptor 位于中间位置,恰好在网络连接建立之前,这是它能拦截网络请求、先查缓存的关键。

2.2 责任链中 CacheInterceptor 的位置与传递流程

下面的流程图清晰展示了 OkHttp 默认拦截器链中 CacheInterceptor 的插入位置以及请求/响应在链上的完整传递路径。

OkHttp 拦截器链与 CacheInterceptor 位置流程图 OkHttp 拦截器链:CacheInterceptor 位置与请求/响应传递流程 RetryAndFollowUpInterceptor BridgeInterceptor CacheInterceptor 缓存读写 · 条件校验 · 缓存更新 ConnectInterceptor CallServerInterceptor 请求传递方向:自上而下 响应返回方向:自下而上(虚线) 应用层 缓存层 网络层

2.3 CacheInterceptor 内部核心流程

当请求到达 CacheInterceptor 时,拦截器会按以下步骤决策:

  1. 读取缓存候选(Cache Candidate):调用 InternalCache.get(request),尝试从磁盘或内存缓存中获取之前缓存的 CacheResponse。
  2. 判断缓存是否可用:根据 CacheStrategy 计算规则(结合 max-age、must-revalidate 等指令)判断缓存是否新鲜。若新鲜则直接返回缓存响应,不再继续向下传递请求(短路返回)。
  3. 构建条件请求:若不新鲜,在原始请求中添加 If-None-Match(ETag)和 If-Modified-Since(Last-Modified)头部,构造条件验证请求。
  4. 传递请求给下一个拦截器:调用 chain.proceed(conditionalRequest),将条件请求交给 ConnectInterceptor 和 CallServerInterceptor 发送到服务端。
  5. 处理网络响应:如果服务端返回 304 Not Modified,则复用缓存数据,返回缓存的响应体;如果返回 200 OK,则使用新响应,并将新响应写入缓存。
  6. 缓存写入:对于可缓存的 200 响应,通过 InternalCache.put(response) 更新缓存,并返回新响应给上一级拦截器。

2.4 关键代码:CacheInterceptor.intercept 骨架

@Override
public Response intercept(Chain chain) throws IOException {
    // 1. 尝试从缓存中获取候选响应
    Response cacheCandidate = cache != null ? cache.get(chain.request()) : null;

    // 2. 计算缓存策略
    CacheStrategy strategy = new CacheStrategy.Factory(System.currentTimeMillis(),
            chain.request(), cacheCandidate).get();
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;

    // 3. 缓存命中且有效:直接返回缓存,不发起网络请求
    if (cacheResponse != null && networkRequest == null) {
        return cacheResponse.newBuilder()
                .cacheResponse(stripBody(cacheResponse))
                .build();
    }

    // 4. 发起网络请求(携带条件验证头部)
    Response networkResponse = null;
    try {
        networkResponse = chain.proceed(networkRequest != null ? networkRequest : chain.request());
    } finally {
        // 如果缓存是非空但有条件验证,清理旧缓存
        if (cacheResponse != null && cacheCandidate == null) {
            closeQuietly(cacheResponse.body());
        }
    }

    // 5. 处理网络响应:304 复用缓存,200 更新缓存
    if (cacheResponse != null && networkResponse.code() == HTTP_NOT_MODIFIED) {
        Response response = cacheResponse.newBuilder()
                .headers(networkResponse.headers())
                .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
                .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
                .cacheResponse(stripBody(cacheResponse))
                .networkResponse(stripBody(networkResponse))
                .build();
        response.body().close();
        cache.trackConditionalCacheHit();
        cache.update(cacheResponse, response);
        return response;
    } else {
        // 新响应,写入缓存
        Response response = networkResponse.newBuilder()
                .cacheResponse(stripBody(cacheResponse))
                .networkResponse(stripBody(networkResponse))
                .build();
        if (cache != null && HttpHeaders.hasBody(response) && cacheStrategy.isCacheable(response, networkRequest)) {
            CacheRequest cacheRequest = cache.put(response);
            return cacheWritingResponse(cacheRequest, response);
        }
        return response;
    }
}
常见错误:初学者容易混淆 networkRequest 和 cacheResponse 的 null 判断逻辑。务必理解 CacheStrategy 输出的两种可能:
• networkRequest == null 且 cacheResponse != null → 缓存新鲜,直接返回缓存。
• 两者均非 null → 需要发送条件请求验证缓存是否过期。
错误的条件判断会导致缓存击穿或重复网络请求。

2.5 调试与验证方法

在开发环境中,可以通过以下方式验证 CacheInterceptor 是否按预期工作:

  • 启用 OkHttp 日志拦截器:在拦截器链中添加 HttpLoggingInterceptor,并设置为 BODY 级别,可以观察到条件请求头部(If-None-Match)是否被正确添加,以及服务端响应状态码是 304 还是 200。
  • 设置断点:在 CacheInterceptor.intercept() 的 cacheResponse 和 networkResponse 分支上打断点,观察 CacheStrategy 的决策结果。
  • 检查缓存文件:在 Android 中使用 adb shell ls -l /data/data/<包名>/cache/okhttp/ 查看缓存目录下的条目数及文件大小。
// 日志拦截器配置示例
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.HEADERS);
OkHttpClient client = new OkHttpClient.Builder()
        .addInterceptor(logging)  // 应用层拦截器,优先执行
        .cache(cache)            // 必须设置 cache,否则 CacheInterceptor 会跳过
        .build();

2.6 性能与稳定性优化建议

关注点 建议措施 影响
缓存读 I/O 延迟 使用 CacheControl.FORCE_CACHE 或增大 max-stale 减少网络请求 提升弱网环境响应速度,但可能返回陈旧数据
缓存写入频率 对 304 响应仅更新头部,避免重复写入 body 减少磁盘写放大,延长闪存寿命
并发请求冲突 确保 InternalCache 实现是线程安全的(OkHttp 内置的 DiskLruCache 已处理) 避免缓存损坏或 OOM
条件请求风暴 合理设置 max-stale 和 must-revalidate,避免大量资源同时发起条件验证 减轻服务端压力,降低带宽消耗

2.7 常见问题排查

问题 1:已设置 cache,但始终不走缓存
原因:CacheControl 指令中指定了 no-cache 或 no-store,导致 CacheStrategy 判定 networkRequest 始终不为 null。
解决:检查请求和响应中的 Cache-Control 头部;确认服务端返回了正确的缓存相关头部(如 Cache-Control: max-age=3600)。
问题 2:条件请求无效,每次返回 200 而非常 304
原因:服务端不支持 ETag/Last-Modified,或者客户端发送的 If-None-Match 值与服务端当前 ETag 不匹配。
解决:使用日志拦截器打印请求头,确认条件头部是否被正确添加;检查服务端响应是否包含 ETag 或 Last-Modified。
注意:OkHttp 的 CacheInterceptor 默认只在首次缓存后缓存 ETag/LM,若服务端未返回,则条件验证将退化。

2.8 小结:责任链设计的好处

将缓存逻辑封装在 CacheInterceptor 中,并通过责任链与网络层解耦,带来了以下优势:

  • 单一职责:每个拦截器只关心自己的事,缓存不会侵入连接复用或请求重试逻辑。
  • 可扩展性:开发者可以自定义拦截器插入链中,实现自定义缓存策略(如内存+磁盘二级缓存)。
  • 可测试性:可以单独为 CacheInterceptor 编写单元测试,模拟 Chain 并验证缓存决策。
  • 短路优化:缓存命中时直接返回,避免网络 I/O 开销,这是责任链模式最实用的性能收益。