2. OkHttp 缓存核心概念:Cache-Control 指令
HTTP 缓存的核心是 Cache-Control 响应头(也可在请求中使用),它定义了缓存的行为规则。OkHttp 完全遵循 HTTP/1.1 缓存规范,将这些指令转化为 CacheControl 对象。理解以下四个关键指令是调试和优化缓存的基础。
2.1 指令概览
| 指令 | 方向 | 含义 |
|---|---|---|
max-age=<秒> | 响应 | 资源自生成起保持新鲜的秒数 |
no-cache | 响应/请求 | 必须与源服务器验证后才能使用缓存 |
must-revalidate | 响应 | 缓存过期后必须与源服务器验证 |
no-store | 响应 | 禁止缓存存储任何副本 |
public | 响应 | 任何缓存(包括CDN)都可以存储 |
private | 响应 | 仅浏览器/本地缓存可以存储 |
2.2 max-age:新鲜度窗口
服务器通过 max-age 声明资源在多长时间内被视为“新鲜”。OkHttp 会直接使用未过期的缓存,不发起网络请求。示例:
Cache-Control: public, max-age=3600
# 该资源在 1 小时内无需再次请求
在 OkHttp 中,你可以在请求时强制忽略 max-age:
Request request = new Request.Builder()
.url("https://api.example.com/user")
.cacheControl(new CacheControl.Builder().noCache().build())
.build();
2.3 no-cache:每次都要验证
尽管名字包含“no-cache”,它并不禁止存储,而是要求每次使用缓存前都向源服务器验证(通过 If-None-Match 或 If-Modified-Since)。服务器返回 304 Not Modified 时,仍可使用缓存内容。
| 响应示例 | Cache-Control: no-cache |
|---|---|
| 实际行为 | OkHttp 会缓存资源,但每次读取前都会发送条件请求 |
注意:如果服务器同时返回
ETag 或 Last-Modified,OkHttp 会自动使用它们进行验证。2.4 must-revalidate:过期后必须验证
该指令通常与 max-age 配合使用。缓存过期后,必须与源服务器验证;即使网络不可用,也不能返回过期的缓存内容(而 proxy-revalidate 仅对共享缓存生效)。
Cache-Control: public, max-age=300, must-revalidate
# 5分钟内直接使用,过期后必须请求服务器
如果没有 must-revalidate,OkHttp 在离线时可能会返回“陈旧”缓存(默认允许的时间窗口为 24 小时)。
2.5 指令对比表格
| 指令 | 是否存储 | 使用前验证 | 离线可用性 |
|---|---|---|---|
max-age | 是 | 仅过期后 | 新鲜期内可用 |
no-cache | 是 | 每次都验证 | 仅当验证成功(304) |
must-revalidate | 是 | 过期后必须验证 | 过期后不可用 |
no-store | 否 | 不适用 | 永不 |
2.6 OkHttp 中的 CacheControl 对象
OkHttp 提供 CacheControl 类以编程方式构造指令。常用方法:
// 服务器返回的 Cache-Control 解析
Response response = client.newCall(request).execute();
CacheControl cacheControl = response.cacheControl();
int maxAge = cacheControl.maxAgeSeconds(); // -1 表示未设置
boolean noCache = cacheControl.noCache();
boolean mustRevalidate = cacheControl.mustRevalidate();
// 构造请求时强制要求服务器返回 no-cache
Request req = new Request.Builder()
.url("...")
.cacheControl(new CacheControl.Builder()
.noCache()
.build())
.build();
查看响应中的 Cache-Control 值最简单的方法是使用 Logging Interceptor,它会打印所有响应头。
2.7 流程图:Cache-Control 决策路径
下图展示了 OkHttp 根据 Cache-Control 指令决定是否使用缓存的完整逻辑:
2.8 调试技巧
- 日志拦截器:使用
HttpLoggingInterceptor并设置级别为HEADERS,即可看到Cache-Control和验证状态。 - 强制禁用缓存:在请求时设置
CacheControl.FORCE_NETWORK,可绕过所有缓存逻辑。 - 查看缓存命中:OkHttp 会在响应中添加
X-Cache或OkHttp-Cache头(需开启日志)。
HttpLoggingInterceptor logging = new HttpLoggingInterceptor(System.out::println);
logging.setLevel(HttpLoggingInterceptor.Level.HEADERS);
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(logging)
.cache(new Cache(cacheDir, 10 * 1024 * 1024))
.build();
2.9 常见错误
| 错误 | 后果 | 解决方法 |
|---|---|---|
max-age 设置过大(如 86400*7) | 用户长时间看不到更新 | 配合 no-cache 或使用 ETag 实现增量更新 |
同时使用 no-cache 和 no-store | 缓存被完全禁用,性能下降 | 明确需求:只需验证用 no-cache,禁止存储用 no-store |
忘记设置 must-revalidate | 离线时可能返回过时数据 | 在关键业务接口上必须添加 must-revalidate |
服务端返回 Cache-Control: private 但客户端期望共享缓存 | 代理/CDN 无法缓存 | 确认缓存策略,或使用 public 覆盖 |
2.10 性能优化建议
- 静态资源(图片、字体、SVG):使用
public, max-age=31536000, immutable,配合文件名 hash 实现长期缓存。 - 动态列表:使用
no-cache并附带ETag或Last-Modified,确保每次验证后更新,减少带宽消耗。 - 关键会话数据:使用
private, max-age=300, must-revalidate,保证 5 分钟内无网络请求,过期后强制验证。 - 避免过度缓存:对于实时性要求高的接口(如余额查询),直接用
no-store或CacheControl.FORCE_NETWORK。
注意:
must-revalidate 与 no-cache 的区别:前者仅在过期后验证,后者每次都要验证。选择不当会导致不必要的网络请求或数据陈旧。