缓存策略对象(CacheStrategy):从 Request/Response 到策略的转换逻辑
CacheStrategy 是 OkHttp 缓存机制的核心决策引擎。它接收原始请求(Request)和已有的缓存条目(CacheEntry),输出三个关键字段:networkRequest、cacheResponse 和 cacheResponse(实际是上一次的响应)。根据这三个字段的空与非空,拦截器决定是发起网络请求、直接返回缓存、组合使用条件请求,还是返回 504。
核心规则:如果
networkRequest 为 null,则跳过网络;如果 cacheResponse 为 null,则无法使用缓存。4.1 原理概述
CacheStrategy 内部遵循 HTTP 缓存规范(RFC 7234),主要判断以下条件:
- 请求的 Cache-Control 指令(如 max-age、no-cache、no-store)
- 响应的 Cache-Control 指令(如 max-age、must-revalidate、public/private)
- 响应中的 Expires 时间与 Date 时间
- Last-Modified 与 ETag 用于条件验证
- 当前系统时间与响应年龄的对比
CacheStrategy 是不可变对象,一旦生成,在整个请求生命周期内不会变更。其创建成本很低,因为只做纯计算,不涉及 I/O。
4.2 转换流程
下图展示了从原始请求 + 缓存条目到策略决策的完整路径:
4.3 核心 API 与实现流程
OkHttp 在 CacheInterceptor 中调用 CacheStrategy.Factory.create() 生成策略对象。关键步骤:
- 从缓存中根据请求 URL 和缓存键获取
CacheEntry(包含原始响应、过期时间等)。 - 调用
new CacheStrategy.Factory(request, cacheResponse)构造工厂。 - 工厂内部解析请求和缓存的 Cache-Control、Expires、Last-Modified 等字段,计算出
networkRequest和cacheResponse。 - 返回不可变的
CacheStrategy对象。
注意:
CacheStrategy.Factory 构造函数第二个参数可以是 null,代表无缓存条目。此时策略将始终返回非空的 networkRequest。4.4 代码示例:策略生成逻辑核心
以下简化代码展示了 CacheStrategy.Factory 的核心决策逻辑(实际源码约 150 ~ 200 行,此处提炼关键分支):
// 伪代码 - 非 OkHttp 源码,仅表达逻辑
public class CacheStrategy {
final @Nullable Request networkRequest;
final @Nullable Response cacheResponse;
public static class Factory {
final Request request;
final @Nullable Response cacheResponse;
public Factory(Request request, @Nullable Response cacheResponse) {
this.request = request;
this.cacheResponse = cacheResponse;
}
public CacheStrategy create() {
Request networkRequestCandidate = null;
Response cacheCandidate = null;
if (cacheResponse == null) {
// 没有缓存,必须发起网络
networkRequestCandidate = request;
} else {
// 检查缓存是否新鲜
if (isFresh(cacheResponse)) {
// 缓存新鲜,不使用网络
cacheCandidate = cacheResponse;
} else {
// 缓存过期,尝试条件请求
networkRequestCandidate = addConditionalHeaders(request, cacheResponse);
cacheCandidate = cacheResponse; // 保留旧缓存供 304 使用
}
}
return new CacheStrategy(networkRequestCandidate, cacheCandidate);
}
private boolean isFresh(Response response) {
// 根据 max-age、Expires、Date 等计算新鲜度
}
private Request addConditionalHeaders(Request request, Response cache) {
// 添加 If-None-Match 或 If-Modified-Since
}
}
}
4.5 调试与日志
在开发阶段可在 CacheInterceptor 中添加日志,打印策略决策结果:
// 在拦截器中添加临时日志
Response cacheResponse = cache.get(url);
CacheStrategy strategy = new CacheStrategy.Factory(request, cacheResponse).create();
Log.d("CacheStrategy",
"networkRequest=" + (strategy.networkRequest != null ? "present" : "null") +
" cacheResponse=" + (strategy.cacheResponse != null ? "present" : "null"));
常见的三种日志输出:
- networkRequest=null, cacheResponse=present → 直接使用缓存,无需网络。
- networkRequest=present, cacheResponse=null → 没有缓存或缓存不可用,发起完整请求。
- networkRequest=present, cacheResponse=present → 发送条件请求,等待 304/200。
4.6 常见错误与陷阱
| 错误场景 | 表现 | 根本原因 |
|---|---|---|
服务端未返回 Cache-Control 或 Expires |
CacheStrategy 认为缓存不可用,每次都发网络请求 | 工厂中新鲜度计算失败,默认使用 never cache 策略 |
| 客户端与服务器时间不同步 | 缓存过早过期或永不过期 | Date 和 Expires 比较基于系统时间,差异大时策略错误 |
Cache-Control: no-store 被忽略 |
缓存依然使用 | OkHttp 默认忽略 no-store?实际会彻底禁用缓存,检查自己是否正确解析 |
| 条件请求后服务端返回 304,但本地缓存已被清理 | 抛出意外空指针 | 网络请求过程中缓存被其他线程删除,需加锁或防御性检查 |
4.7 性能与稳定性优化建议
- 避免重复创建 Factory:每个请求只创建一次,Factory 内无 I/O,开销很小,但需注意在拦截器链中不要多次调用。
- 时间戳精度:使用
System.currentTimeMillis()计算新鲜度,在低性能 IoT 设备上误差较大;可考虑使用System.nanoTime()做相对时间。 - 缓存键一致性:URL 中即使 query 顺序不同也会被视为不同缓存条目,建议在配置缓存时统一排序。
- 防御性空值:Factory 的构造函数参数允许
null,但在create()方法中请确保不直接使用空指针。
如果在自定义 Interceptor 中直接操作 CacheStrategy,请记得保留原始
cacheResponse 引用,因为条件请求 304 时需要用其组装最终响应。