第1章 拦截器基础与缓存核心原理

在 OkHttp 中,拦截器(Interceptor)是整个请求生命周期的核心扩展点。它允许你在请求发送前、响应返回后、以及重试与重定向过程中插入自定义逻辑。理解拦截器链的运转机制以及缓存策略的优先级,是高效配置 OkHttp 缓存的根基。

OkHttp 拦截器链与缓存流程 OkHttp 拦截器链与缓存核心流程 客户端请求 Application Interceptors (1号链) 对请求/响应完全可见 可观察重定向、重试 缓存策略 强制/协商 缓存命中 直接返回缓存 Network Interceptors (2号链) 仅当实际网络请求时执行 服务器 📌 拦截器执行顺序: 请求:Application → 缓存 → Network → 服务器 响应:逆向回传

1.1 拦截器类型:Application vs Network

OkHttp 内置两条拦截器链,它们在请求生命周期中的位置和可见性不同。下表总结了核心差异:

维度 Application Interceptor Network Interceptor
注册方式 addInterceptor() addNetworkInterceptor()
触发时机 缓存命中前、重试和重定向都会重新进入 只在真正发起网络请求时执行(重定向、重试时不会重复)
可见范围 可观察到 curl 级别的原始请求/响应(包括重定向结果) 可观察到一次实际网络请求的中间结果
适用场景 日志、Header 注入、统一认证、缓存控制 网络重定向跟踪、自定义 DNS、连接池监控
⚠️ 常见误区: 许多人将缓存相关逻辑只放在 Network Interceptor 中,导致缓存命中时 Network 拦截器不会被调用,容易遗漏条件请求头处理。缓存拦截通常应放在 Application 层级。

1.2 缓存机制:Cache-Control 与缓存策略

OkHttp 的缓存行为由 HTTP 响应头 Cache-Control 主导,同时遵循强制缓存和协商缓存两个层级。

1.2.1 强制缓存(Cache-Control: max-age)

在有效期内,客户端直接使用本地缓存,不与服务器通信。

  • max-age=N:从请求时刻起,N 秒内直接使用缓存。
  • immutable:暗示不会更新,常用于静态资源。
  • no-cache:不允许强制缓存,必须与服务器协商。
  • no-store:禁止所有缓存。

1.2.2 协商缓存(ETag / Last-Modified)

强制缓存失效后,客户端携带条件请求头询问服务器资源是否变化:

  • If-None-Match:与服务器 ETag 比较,匹配则返回 304 Not Modified。
  • If-Modified-Since:与服务器 Last-Modified 比较,返回 304 或新资源。

OkHttp 内部自动处理 304 响应,用缓存副本更新并返回给上层。

1.2.3 缓存策略优先级(从高到低)

  1. Cache-Control: no-store —— 禁用缓存
  2. Cache-Control: no-cache —— 强制协商
  3. Cache-Control: max-age 或 Expires —— 强制缓存(Expires 优先级低于 max-age)
  4. ETag / Last-Modified —— 协商缓存(ETag 优先于 Last-Modified)
  5. 用户自定义 / 请求端 Cache-Control —— 客户端可通过 CacheControl.FORCE_NETWORK 或 CacheControl.FORCE_CACHE 覆盖

1.3 实现流程:开启缓存并配置拦截器

以下代码展示如何在 OkHttp 中开启缓存并注册 Application 拦截器:

// 1. 创建缓存目录(通常放在应用专属缓存目录)
File cacheDir = new File(getCacheDir(), "okhttp_cache");
int cacheSize = 50 * 1024 * 1024; // 50 MB

// 2. 构建 Cache 对象
Cache cache = new Cache(cacheDir, cacheSize);

// 3. 自定义 Application 拦截器(用于日志 / 缓存策略)
Interceptor appInterceptor = chain -> {
    Request request = chain.request();
    // 示例:为请求添加 Cache-Control 头(强制使用缓存)
    Request newRequest = request.newBuilder()
            .cacheControl(CacheControl.FORCE_CACHE)
            .build();
    Response response = chain.proceed(newRequest);
    // 日志输出
    System.out.println("Response code: " + response.code());
    return response;
};

// 4. 构建 OkHttpClient
OkHttpClient client = new OkHttpClient.Builder()
        .cache(cache)
        .addInterceptor(appInterceptor)             // Application 层级
        .addNetworkInterceptor(interceptor2)         // Network 层级(可选)
        .build();

1.4 常见错误与调试要点

  • 缓存目录未创建: getCacheDir() 返回的目录不存在时会抛出 IOException,建议先执行 mkdirs()。
  • 拦截器顺序错误: 在 Application 拦截器中修改请求头后,Network 拦截器可能收到修改后的请求;但如果同时使用了 addInterceptor 和 addNetworkInterceptor,注意响应经过的链顺序。
  • 缓存未生效: 检查服务器响应是否包含 Cache-Control 头,以及客户端是否正确配置了 Cache 对象。
  • 调试技巧: 打印 response.cacheResponse() 和 response.networkResponse() 可观察缓存来源。
// 调试示例:打印缓存命中信息
Response response = client.newCall(request).execute();
if (response.cacheResponse() != null) {
    System.out.println("Response from CACHE: " + response.cacheResponse().code());
} else if (response.networkResponse() != null) {
    System.out.println("Response from NETWORK: " + response.networkResponse().code());
}
📌 性能优化提示: 强制缓存可大幅降低网络开销,但需合理设置 max-age 值。对于动态 API,建议使用 no-cache + ETag 组合,既保证及时更新又减少带宽消耗。

1.5 部署时注意事项

  • 生产环境中缓存目录应使用应用私有目录,避免被系统清理。
  • 缓存大小应根据设备可用空间调整,一般设为请求资源总大小的 2~3 倍。
  • 若使用 CacheControl.FORCE_CACHE 且无有效缓存,请求将直接失败,此时应先检查是否存在缓存。