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 指令决定是否使用缓存的完整逻辑:

Cache-Control 决策流程 OkHttp 缓存决策流程 (Cache-Control) 客户端发起请求 检查本地缓存 有缓存? 否 向源服务器请求 是 检查 max-age / 新鲜度 新鲜? 是 直接返回缓存 检查 must-revalidate 设置了? 是 必须向服务器验证 否 允许使用陈旧缓存 max-age / no-cache 影响判断

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 的区别:前者仅在过期后验证,后者每次都要验证。选择不当会导致不必要的网络请求或数据陈旧。