第2章 OkHttp缓存拦截器核心接口与工作机制:源码分析 Interceptor.Chain、CacheStrategy、CacheControl

本章要点:从源码层剖析 OkHttp 拦截器链的三大核心组件——Interceptor.Chain、CacheStrategy、CacheControl,理解它们如何协作实现缓存命中、失效判断与请求再验证,并掌握拦截器调用链的完整执行路径。

2.1 拦截器链的整体架构

OkHttp 的缓存能力建立在一套分层拦截器架构之上。每个网络请求在发出前会依次穿过多个拦截器,其中 CacheInterceptor 是决定「是否走缓存」的关键一环。理解它必须从最顶层的 Interceptor.Chain 接口开始。

组件角色源码包路径
Interceptor.Chain拦截器链调度接口,控制请求传递与响应回溯okhttp3.Interceptor
CacheStrategy缓存策略决策器,判定缓存是否可用、是否需要网络请求okhttp3.internal.cache.CacheStrategy
CacheControl解析与构建 HTTP Cache-Control 指令的不可变对象okhttp3.CacheControl
CacheInterceptor拦截器链中的缓存处理节点,组合上述三个组件okhttp3.internal.cache.CacheInterceptor

2.2 Interceptor.Chain 接口:请求传递的调度中枢

Interceptor.Chain 定义了两个核心方法,是所有拦截器编排的基础:

public interface Interceptor {
  Response intercept(Chain chain);

  interface Chain {
    Request request();
    Response proceed(Request request);
    Connection connection();
    int connectTimeoutMillis();
    Chain withConnectTimeout(int timeout, TimeUnit unit);
    int readTimeoutMillis();
    Chain withReadTimeout(int timeout, TimeUnit unit);
    int writeTimeoutMillis();
    Chain withWriteTimeout(int timeout, TimeUnit unit);
  }
}

每次调用 chain.proceed(request) 时,RealInterceptorChain 会将请求递交给列表中的下一个拦截器,同时保留响应回溯的能力。缓存拦截器正是利用这一机制,在适当时机直接返回缓存响应,从而短路后续的网络拦截器。

关键设计:proceed() 是同步调用,每个拦截器在调用前后可分别执行「请求前处理」和「响应后处理」。CacheInterceptor 在调用 proceed() 之前检查缓存,在调用之后更新缓存。

2.3 CacheStrategy:缓存命中的决策引擎

CacheStrategy 是纯计算类,不持有状态。它的核心方法 get() 接收 Request 和 Response(缓存条目),返回一个 CacheStrategy 实例,其中包含两个字段:

public final class CacheStrategy {
  public final @Nullable Request networkRequest;   // null 表示不需网络请求
  public final @Nullable Response cacheResponse;   // null 表示无可用缓存

  // 核心工厂方法
  public static CacheStrategy get(Request request, Response cacheResponse) {
    // 根据 Cache-Control、Date、Expires、Last-Modified、ETag 等头部计算
    // 返回值有三种组合:
    //   1. networkRequest != null, cacheResponse == null  → 仅网络请求
    //   2. networkRequest == null, cacheResponse != null  → 仅缓存响应(缓存命中)
    //   3. networkRequest != null, cacheResponse != null  → 条件请求(再验证)
  }
}
三种决策结果:
  • 仅网络请求:无缓存 / 缓存已过期且无验证条件 → 走完整网络链路
  • 仅缓存响应:缓存新鲜(max-age 有效期内)→ 直接返回缓存,完全不发起网络请求
  • 条件请求:缓存过期但有 ETag/Last-Modified → 发起 GET 请求携带 If-None-Match 或 If-Modified-Since

2.4 CacheControl:HTTP 缓存指令的封装

CacheControl 解析 Cache-Control 与 Pragma 头部,对外提供布尔/整型查询方法。它同时服务于请求指令(客户端控制)和响应指令(服务端控制)。

public final class CacheControl {
  public static final CacheControl FORCE_NETWORK = new CacheControl.Builder()
      .noCache().build();       // 等价于 Cache-Control: no-cache
  public static final CacheControl FORCE_CACHE = new CacheControl.Builder()
      .onlyIfCached().maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS).build();

  private final boolean noCache;          // 每次都必须验证
  private final boolean noStore;          // 不允许缓存任何内容
  private final int maxAgeSeconds;        // 缓存新鲜期(秒)
  private final int maxStaleSeconds;      // 过期后仍可服侍的最大秒数
  private final int minFreshSeconds;      // 要求剩余新鲜时间至少为…
  private final boolean onlyIfCached;     // 只接受缓存,不发起网络
  private final boolean mustRevalidate;   // 服务端要求必须验证

  // 解析头部
  public static CacheControl parse(Headers headers) { /* … */ }
  // 构建请求/响应指令
  public static final class Builder { /* … */ }
}
指令作用于含义CacheInterceptor 行为
no-cache请求 / 响应每次必须向源服务器验证强制走 proceed(),但可携带条件头
no-store请求 / 响应禁止写入缓存不读取也不写入磁盘缓存
max-age=60响应60 秒内为新鲜60 秒内直接返回缓存,不发请求
only-if-cached请求只接受缓存,无缓存则返回 504缓存未命中时直接构造 504 响应
max-stale=30请求过期后 30 秒内仍可接受扩展新鲜期,减少网络请求

2.5 拦截器调用链与三组件协作流程

下图完整展示了从请求发起经 RealInterceptorChain 调度,逐层穿过各拦截器,并在 CacheInterceptor 处利用 CacheStrategy + CacheControl 做出缓存决策的全过程。

OkHttp 拦截器调用链与缓存核心机制 OkHttp 拦截器调用链与缓存核心机制 客户端请求 RealInterceptor Chain App 拦截器 CacheInterceptor 缓存决策核心 CacheStrategy CacheControl ConnectInterceptor Net 拦截器 服务端 响应回溯(Response) 拦截器调用链执行说明 ① 请求由 RealInterceptorChain 按顺序传递给每个拦截器的 intercept() 方法; ② CacheInterceptor 在调用 proceed() 之前,利用 CacheStrategy 判断缓存是否可用; ③ 若缓存新鲜(受 CacheControl 的 max-age/max-stale 控制),直接返回缓存响应,不再执行后续拦截器; ④ 否则调用 proceed() 继续传递,响应回溯时再写回缓存。

2.6 源码关键流程解析

在 CacheInterceptor.intercept() 内部,三组件的协作顺序非常清晰:

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

  // 2. 构建 CacheStrategy —— 传入当前请求 + 缓存候选响应
  CacheStrategy strategy = CacheStrategy.get(chain.request(), cacheCandidate);

  // 3. 根据策略结果判断
  if (strategy.networkRequest == null && strategy.cacheResponse == null) {
    // 既无网络请求也无缓存 → 504
    return new Response.Builder()
        .code(504)
        .message("Unsatisfiable Request (only-if-cached)")
        .body(ResponseBody.create(null, ""))
        .build();
  }

  if (strategy.networkRequest == null) {
    // 无需网络请求 → 直接返回缓存(缓存命中)
    return strategy.cacheResponse.newBuilder()
        .cacheResponse(stripBody(strategy.cacheResponse))
        .build();
  }

  // 4. 需要网络请求 —— 继续传递拦截器链
  Response networkResponse;
  try {
    networkResponse = chain.proceed(strategy.networkRequest);
  } catch (IOException e) {
    // 网络失败时,若有可用过期缓存则降级使用
    if (cacheCandidate != null) throw e;
    throw e;
  }

  // 5. 响应回溯后更新缓存
  if (cache != null) {
    if (HttpHeaders.promisesBodyForResponse(networkResponse)) {
      CacheStrategy.Companion.trackResponse(cache, strategy);
    }
  }
  return networkResponse;
}

2.7 调试与日志验证

启 OkHttp 的日志拦截器可以直观看到缓存决策过程:

// 添加日志拦截器观察缓存行为
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.HEADERS);

OkHttpClient client = new OkHttpClient.Builder()
    .addInterceptor(logging)          // 应用层:看到请求/响应头部
    .cache(new Cache(cacheDir, 10L * 1024 * 1024))
    .build();

// 日志输出示例(缓存命中时):
// → GET https://api.example.com/users
// ← 200 (cache)  → 注意 "(cache)" 标记表示未发起网络请求
// → Cache-Control: max-age=3600
// ← Date: Thu, 01 Jan 2025 12:00:00 GMT
调试技巧:使用 CacheControl.FORCE_NETWORK 可临时绕过缓存验证网络连通性;使用 CacheControl.FORCE_CACHE 可测试离线缓存表现。

2.8 常见错误与稳定性优化

错误模式现象根因解决方案
缓存永远不命中每条日志都无 (cache) 标记响应头缺少 Cache-Control 或设置了 no-store在服务端添加 Cache-Control: max-age=300
缓存数据陈旧展示过期内容max-stale 设置过大或服务端 must-revalidate 被忽略合理配置 max-age 并启用 must-revalidate
条件请求频繁每次仍发起网络请求响应缺少 ETag 或 Last-Modified服务端添加验证器头部,减少传输成本
缓存写回异常日志显示 504 或缓存文件损坏磁盘空间满或 Cache 目录权限错误监控 Cache.size(),捕获 IOException 降级
性能红线:切勿在 CacheInterceptor 之前添加大量耗时拦截器,否则即使缓存命中也会引入不必要的延迟。推荐的拦截器顺序:日志 → 重试 → 缓存 → 连接 → 网络。

2.9 本章总结

  • Interceptor.Chain 是拦截器编排的基石,proceed() 控制请求传递与响应回溯;
  • CacheStrategy 是纯函数式决策器,三种输出对应三种网络行为;
  • CacheControl 封装 HTTP 缓存指令,同时服务客户端与服务端;
  • 三者通过 CacheInterceptor 整合,在调用链中实现短路网络、条件验证、缓存更新完整闭环。