2. Cache-Control 指令详解:max-age、s-maxage、no-cache、no-store、must-revalidate

2.1 指令原理与缓存决策模型

Cache-Control 是 HTTP/1.1 中定义的缓存控制头部,它通过一组指令(directives)告诉客户端、代理服务器以及中间缓存如何处理响应和请求。在 OkHttp 磁盘缓存中,这些指令直接决定了缓存能否被存储、能否被复用以及何时需要向服务器验证。

核心原则: 服务器通过 Cache-Control 响应头声明资源的可缓存性和新鲜度;客户端(包括 OkHttp)必须严格遵守这些指令,仅在指令允许的前提下使用本地缓存。

下表总结了本节五个关键指令的语义和作用范围:

指令 语义 作用域 OkHttp 缓存行为
max-age=<seconds> 资源从生成时刻起,在 <seconds> 秒内是新鲜的,可直接使用缓存 共享缓存 + 私有缓存 在 max-age 内直接返回缓存,不发起网络请求
s-maxage=<seconds> 仅适用于共享缓存(如 CDN、反向代理),覆盖 max-age 仅共享缓存 OkHttp 作为客户端属于私有缓存,忽略 s-maxage
no-cache 禁止直接使用缓存,每次使用前必须先向源服务器验证(条件请求) 共享 + 私有 每次都会发起网络请求(若服务端返回 304 则使用缓存内容)
no-store 禁止任何缓存(包括浏览器和代理)存储资源的任何副本 共享 + 私有 OkHttp 完全跳过磁盘缓存,不存储也不读取
must-revalidate 缓存过期后必须向源服务器验证,不可使用过期缓存 共享 + 私有 缓存过期后强制发起条件请求,不允许使用过期的 stale 缓存

2.2 指令优先级与组合规则

当多个指令同时出现时,OkHttp 按照以下优先级解析:

  1. no-store > 一切:直接跳过缓存。
  2. no-cache > max-age:即使 max-age 很大,也要强制重新验证。
  3. s-maxage 仅在共享缓存中覆盖 max-age,OkHttp 忽略。
  4. must-revalidate 仅在缓存过期后生效:若缓存未过期,资源可直接使用。
调试建议: 使用 Chrome DevTools Network 面板的 Response Headers 或 curl -I 查看服务端返回的 Cache-Control 字段,快速判断每个资源的缓存策略。

2.3 流程图:Cache-Control 缓存决策路径

下面的 SVG 图展示了 OkHttp 收到响应时,根据 Cache-Control 指令决定是否缓存以及缓存使用流程:

Cache-Control 缓存决策流程图 (OkHttp 视角) 收到响应 no-store? 不存储、不读取 no-cache? 条件请求 max-age (新鲜期内?) 直接返回缓存 must-revalidate? 强制验证 可能使用过期缓存 s-maxage 仅在共享缓存生效 OkHttp 作为客户端忽略它

2.4 配置实战:OkHttp 如何使用 Cache-Control

OkHttp 的 Cache 模块内置了完整的 HTTP 缓存协定支持。以下是三种典型配置场景:

2.4.1 服务端响应头示例(后端需设置)

// 示例1:私有缓存,1小时新鲜期
Cache-Control: private, max-age=3600

// 示例2:禁止缓存(如登录API)
Cache-Control: no-store

// 示例3:强制每次验证,但允许有条件使用缓存
Cache-Control: no-cache, must-revalidate

// 示例4:共享缓存优先,私有缓存使用备选时长
Cache-Control: public, s-maxage=600, max-age=300

2.4.2 OkHttp 客户端代码:设置缓存大小并生效

// 创建缓存目录和 Cache 对象
File cacheDir = new File(context.getCacheDir(), "http_cache");
int cacheSize = 10 * 1024 * 1024; // 10 MB
Cache cache = new Cache(cacheDir, cacheSize);

// 配置 OkHttpClient
OkHttpClient client = new OkHttpClient.Builder()
    .cache(cache)
    .build();

// 请求时自动处理 Cache-Control, 无需额外配置
Request request = new Request.Builder()
    .url("https://api.example.com/profile")
    .build();

Response response = client.newCall(request).execute();
// 如果服务端返回 Cache-Control: max-age=3600
// 则后续1小时内同URL请求直接返回缓存,不会触发网络

2.4.3 强制刷新缓存(客户端覆盖服务端指令)

某些场景需要客户端主动忽略缓存(例如用户下拉刷新),可通过请求头覆盖:

Request refreshRequest = new Request.Builder()
    .url("https://api.example.com/profile")
    .header("Cache-Control", "no-cache")   // 告诉代理/服务端不用缓存响应
    .build();
注意: 客户端设置的 Cache-Control 请求头仅对中间共享缓存有约束力,OkHttp 自身的缓存策略仍以响应头为准。如需绕过 OkHttp 缓存,请使用 CacheControl.FORCE_NETWORK。

2.5 常见错误与性能陷阱

错误场景 表现 根本原因 解决方案
服务端未设置 Cache-Control OkHttp 仍会使用启发式缓存(最多 10% 时间) 缺少显式指令,默认行为不可预测 服务端显式添加 Cache-Control: max-age=0, must-revalidate
混用 no-cache 和 max-age 每次请求都发网络请求(304) no-cache 优先级高于 max-age 只要需要验证就用 no-cache;不要同时写 max-age
忽略 must-revalidate OkHttp 默认允许使用过期缓存(stale-while-revalidate) OkHttp 有默认的 stale 容忍机制 若不允许过期缓存,务必加 must-revalidate
将 s-maxage 用于客户端缓存配置 缓存行为不符合预期 OkHttp 忽略 s-maxage 客户端场景统一使用 max-age

2.6 稳定性与调试建议

  • 启用 OkHttp 日志拦截器:观察每次请求是 命中缓存 还是 网络请求。
    HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
    logging.setLevel(HttpLoggingInterceptor.Level.HEADERS);
    client = client.newBuilder()
        .addInterceptor(logging)
        .build();
  • 使用 CacheControl.FORCE_NETWORK 和 FORCE_CACHE:在测试阶段强制验证缓存策略是否符合预期。
    Request forceNetworkRequest = request.newBuilder()
        .cacheControl(CacheControl.FORCE_NETWORK)
        .build();
    Response response = client.newCall(forceNetworkRequest).execute();
  • 检查磁盘缓存目录:Android 中 getCacheDir()/http_cache 下会生成 journal 和 .0/.1 文件,文件大小反映缓存使用量。

2.7 总结

  • max-age 控制新鲜度,是日常最常用的指令。
  • s-maxage 专用于 CDN/代理,客户端不需要处理。
  • no-cache 强制验证,适合频繁变动的资源。
  • no-store 完全禁止缓存,适合安全敏感或一次性数据。
  • must-revalidate 配合 max-age 使用,确保过期后一定验证。
最佳实践: 静态资源(图片、JS、CSS)使用 public, max-age=31536000, immutable;动态 API 使用 private, no-cache, must-revalidate。