第2章:缓存拦截器核心原理:响应缓存、请求缓存、CacheStrategy策略
缓存拦截器是 OkHttp 网络栈中提升性能、降低延迟的关键组件。理解其核心原理,需要先区分两种缓存对象:响应缓存和请求缓存,并掌握 CacheStrategy 策略的决策逻辑。本章从原理到实现,逐步拆解拦截器如何利用 HTTP 缓存头与本地缓存存储协同工作。
2.1 响应缓存与请求缓存的本质区别
| 维度 | 响应缓存(Response Cache) | 请求缓存(Request Cache) |
|---|---|---|
| 存储对象 | 完整的 HTTP 响应体 + 响应头 | 键值对:请求 URL + 请求头 → 缓存条目 |
| 生命周期 | 由响应头 Cache-Control 控制 | 由开发者显式管理(如 LRU 清理) |
| 重新验证触发 | 自动通过 If-Modified-Since / ETag | 手动判断或使用拦截器逻辑 |
| OkHttp 原生支持 | 通过 Cache 类 + CacheInterceptor | 需自定义拦截器实现 |
大多数教材只讨论响应缓存,但在动态缓存拦截器场景中,我们同时需要操作两种缓存:响应缓存用于复用服务器授权过的资源;请求缓存用于存储可安全复用的幂等请求结果(如 GET 请求)。
2.2 CacheStrategy 策略核心
CacheStrategy 是 OkHttp 内部用于决定“是否使用缓存”的决策器。它基于 RFC 7234 规范,将请求与缓存响应配对后输出三种结果:
- Fully Cached:强制缓存有效,直接返回缓存,不发起任何网络请求。
- Conditionally Cached:缓存已过期但存在验证器(Last-Modified/ETag),发起条件式验证请求。
- Not Cached:无法使用当前缓存,必须重新请求。
关键接口:
CacheStrategy.get() 接收 Request 与 CacheCandidate,返回 CacheStrategy 对象。该对象携带 networkRequest 和 cacheResponse 两个字段,分别表示“是否发请求”和“是否用缓存”。
2.3 实现流程:OkHttp 缓存拦截器内部逻辑
- 拦截器从 OkHttpClient 的
Cache对象中根据请求 URL 与请求头计算缓存 Key。 - 从磁盘缓存中读取
CacheEntry,还原为Response(包含缓存头)。 - 调用
CacheStrategy.get()进行策略决策。 - 根据策略决定:
- 若策略要求“仅缓存”:直接返回缓存的 Response。
- 若策略要求“仅网络”:清理缓存引用,调用
chain.proceed()发起网络请求,并在返回前写入新缓存。 - 若策略要求“条件验证”:构造
If-None-Match或If-Modified-Since请求头,发送给服务器;服务器返回 304 则复用缓存的响应,否则使用新响应并更新缓存。
2.4 代码实现:CacheStrategy 的核心参与类
// 简化表达,实际位于 okhttp3.internal.cache 包中
public final class CacheStrategy {
public final @Nullable Request networkRequest; // 非空则需发网络请求
public final @Nullable Response cacheResponse; // 非空则可使用该缓存
public static CacheStrategy get(Request request, Response cacheCandidate) {
// 1. 如果缓存为 null → 只能走网络
if (cacheCandidate == null) {
return new CacheStrategy(request, null);
}
// 2. 检查缓存头是否禁止缓存(no-cache / no-store)
if (cacheCandidate.cacheControl().noCache()) {
return new CacheStrategy(request, null);
}
// 3. 强制缓存有效期检查
long ageMillis = System.currentTimeMillis() - cacheCandidate.sentRequestAtMillis();
long maxAge = cacheCandidate.cacheControl().maxAgeSeconds() * 1000L;
if (ageMillis < maxAge) {
// 强制缓存有效 → 直接返回缓存
return new CacheStrategy(null, cacheCandidate);
}
// 4. 存在验证器 → 条件缓存
String lastModified = cacheCandidate.header("Last-Modified");
String etag = cacheCandidate.header("ETag");
if (lastModified != null || etag != null) {
Request.Builder conditional = request.newBuilder();
if (lastModified != null) {
conditional.header("If-Modified-Since", lastModified);
}
if (etag != null) {
conditional.header("If-None-Match", etag);
}
return new CacheStrategy(conditional.build(), cacheCandidate);
}
// 5. 无法满足任何缓存条件 → 走网络,丢弃原缓存
return new CacheStrategy(request, null);
}
}
2.5 调试与常见错误
| 常见错误 | 原因 | 解决方案 |
|---|---|---|
| 缓存未生效 | 服务器未返回 Cache-Control 头,或拦截器未配置 Cache 对象 | 在 OkHttpClient.Builder 中设置 cache(new Cache(cacheDir, maxSize)) |
| 缓存响应被篡改 | 多线程并发写入同一缓存 Key | 使用 synchronized 或 ReadWriteLock 保护缓存写入 |
| 条件缓存无限循环 | 服务器始终返回 200 而非 304,缓存策略错误 | 检查服务器是否正确支持 If-None-Match,并确保 ETag 值不变 |
| 动态 URL 缓存命中期偏低 | URL 包含随机参数或时间戳 | 自定义缓存 Key 生成器,摘除动态参数 |
调试技巧:启用 OkHttp 日志拦截器,观察
CacheStrategy 输出的 networkRequest 和 cacheResponse 状态。若 networkRequest 为 null 说明强制缓存命中;若 cacheResponse 为 null 说明完全不走缓存。
2.6 性能与稳定性优化
- 缓存读写使用 NIO 内存映射:避免频繁的磁盘 I/O 导致 UI 线程卡顿。
- 设置合理的缓存大小:建议根据应用数据量设定 10MB~50MB,并监控
Cache.evictions()数量避免频繁淘汰。 - 缓存过期前预刷新:在缓存 TTL 还剩 10% 时异步提前发起网络请求更新缓存(Stale-while-revalidate 模式)。
- 避免缓存雪崩:对同一资源的并发请求应加锁或使用
CompletableFuture合并,只让一个线程去更新缓存。