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 按照以下优先级解析:
- no-store > 一切:直接跳过缓存。
- no-cache > max-age:即使 max-age 很大,也要强制重新验证。
- s-maxage 仅在共享缓存中覆盖 max-age,OkHttp 忽略。
- must-revalidate 仅在缓存过期后生效:若缓存未过期,资源可直接使用。
调试建议: 使用 Chrome DevTools Network 面板的 Response Headers 或
curl -I 查看服务端返回的 Cache-Control 字段,快速判断每个资源的缓存策略。
2.3 流程图:Cache-Control 缓存决策路径
下面的 SVG 图展示了 OkHttp 收到响应时,根据 Cache-Control 指令决定是否缓存以及缓存使用流程:
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。