第2章:缓存拦截器核心原理:响应缓存、请求缓存、CacheStrategy策略

缓存拦截器是 OkHttp 网络栈中提升性能、降低延迟的关键组件。理解其核心原理,需要先区分两种缓存对象:响应缓存和请求缓存,并掌握 CacheStrategy 策略的决策逻辑。本章从原理到实现,逐步拆解拦截器如何利用 HTTP 缓存头与本地缓存存储协同工作。

CacheStrategy 策略决策流程 CacheStrategy 策略决策流程 请求缓存查找 是否命中缓存? (检查缓存Key与失效时间) 是 强制缓存有效? (max-age / s-maxage 未过期) 是 直接返回缓存响应 否 条件缓存请求 (If-Modified-Since / ETag) 否 发起网络请求 缓存策略决策 由 CacheStrategy 提供

2.1 响应缓存与请求缓存的本质区别

维度响应缓存(Response Cache)请求缓存(Request Cache)
存储对象完整的 HTTP 响应体 + 响应头键值对:请求 URL + 请求头 → 缓存条目
生命周期由响应头 Cache-Control 控制由开发者显式管理(如 LRU 清理)
重新验证触发自动通过 If-Modified-Since / ETag手动判断或使用拦截器逻辑
OkHttp 原生支持通过 Cache 类 + CacheInterceptor需自定义拦截器实现

大多数教材只讨论响应缓存,但在动态缓存拦截器场景中,我们同时需要操作两种缓存:响应缓存用于复用服务器授权过的资源;请求缓存用于存储可安全复用的幂等请求结果(如 GET 请求)。

2.2 CacheStrategy 策略核心

CacheStrategy 是 OkHttp 内部用于决定“是否使用缓存”的决策器。它基于 RFC 7234 规范,将请求与缓存响应配对后输出三种结果:

  • Fully Cached:强制缓存有效,直接返回缓存,不发起任何网络请求。
  • Conditionally Cached:缓存已过期但存在验证器(Last-Modified/ETag),发起条件式验证请求。
  • Not Cached:无法使用当前缓存,必须重新请求。
关键接口:CacheStrategy.get() 接收 Request 与 CacheCandidate,返回 CacheStrategy 对象。该对象携带 networkRequest 和 cacheResponse 两个字段,分别表示“是否发请求”和“是否用缓存”。

2.3 实现流程:OkHttp 缓存拦截器内部逻辑

  1. 拦截器从 OkHttpClient 的 Cache 对象中根据请求 URL 与请求头计算缓存 Key。
  2. 从磁盘缓存中读取 CacheEntry,还原为 Response(包含缓存头)。
  3. 调用 CacheStrategy.get() 进行策略决策。
  4. 根据策略决定:
    • 若策略要求“仅缓存”:直接返回缓存的 Response。
    • 若策略要求“仅网络”:清理缓存引用,调用 chain.proceed() 发起网络请求,并在返回前写入新缓存。
    • 若策略要求“条件验证”:构造 If-None-Match 或 If-Modified-Since 请求头,发送给服务器;服务器返回 304 则复用缓存的响应,否则使用新响应并更新缓存。

2.4 代码实现:CacheStrategy 的核心参与类

// 简化表达,实际位于 okhttp3.internal.cache 包中
public final class CacheStrategy {
  public final @Nullable Request networkRequest;   // 非空则需发网络请求
  public final @Nullable Response cacheResponse;   // 非空则可使用该缓存

  public static CacheStrategy get(Request request, Response cacheCandidate) {
    // 1. 如果缓存为 null → 只能走网络
    if (cacheCandidate == null) {
      return new CacheStrategy(request, null);
    }

    // 2. 检查缓存头是否禁止缓存(no-cache / no-store)
    if (cacheCandidate.cacheControl().noCache()) {
      return new CacheStrategy(request, null);
    }

    // 3. 强制缓存有效期检查
    long ageMillis = System.currentTimeMillis() - cacheCandidate.sentRequestAtMillis();
    long maxAge = cacheCandidate.cacheControl().maxAgeSeconds() * 1000L;
    if (ageMillis < maxAge) {
      // 强制缓存有效 → 直接返回缓存
      return new CacheStrategy(null, cacheCandidate);
    }

    // 4. 存在验证器 → 条件缓存
    String lastModified = cacheCandidate.header("Last-Modified");
    String etag = cacheCandidate.header("ETag");
    if (lastModified != null || etag != null) {
      Request.Builder conditional = request.newBuilder();
      if (lastModified != null) {
        conditional.header("If-Modified-Since", lastModified);
      }
      if (etag != null) {
        conditional.header("If-None-Match", etag);
      }
      return new CacheStrategy(conditional.build(), cacheCandidate);
    }

    // 5. 无法满足任何缓存条件 → 走网络,丢弃原缓存
    return new CacheStrategy(request, null);
  }
}

2.5 调试与常见错误

常见错误原因解决方案
缓存未生效服务器未返回 Cache-Control 头,或拦截器未配置 Cache 对象在 OkHttpClient.Builder 中设置 cache(new Cache(cacheDir, maxSize))
缓存响应被篡改多线程并发写入同一缓存 Key使用 synchronized 或 ReadWriteLock 保护缓存写入
条件缓存无限循环服务器始终返回 200 而非 304,缓存策略错误检查服务器是否正确支持 If-None-Match,并确保 ETag 值不变
动态 URL 缓存命中期偏低URL 包含随机参数或时间戳自定义缓存 Key 生成器,摘除动态参数
调试技巧:启用 OkHttp 日志拦截器,观察 CacheStrategy 输出的 networkRequest 和 cacheResponse 状态。若 networkRequest 为 null 说明强制缓存命中;若 cacheResponse 为 null 说明完全不走缓存。

2.6 性能与稳定性优化

  • 缓存读写使用 NIO 内存映射:避免频繁的磁盘 I/O 导致 UI 线程卡顿。
  • 设置合理的缓存大小:建议根据应用数据量设定 10MB~50MB,并监控 Cache.evictions() 数量避免频繁淘汰。
  • 缓存过期前预刷新:在缓存 TTL 还剩 10% 时异步提前发起网络请求更新缓存(Stale-while-revalidate 模式)。
  • 避免缓存雪崩:对同一资源的并发请求应加锁或使用 CompletableFuture 合并,只让一个线程去更新缓存。