4. CacheStrategy 缓存决策器:请求与响应的缓存策略生成
4.1 缓存决策器的作用
- 策略生成:根据原始请求的
Cache-Control、If-Modified-Since、If-None-Match等头部以及存储的缓存响应,生成两种候选策略:
① 使用缓存(cacheResponse不为 null)
② 发起网络请求(networkRequest不为 null)。 - 优先级判断:当
cacheResponse和networkRequest同时存在时,由调用方(例如Interceptor)决定优先走网络还是缓存。 - 决策结果:最终决定本次请求是否使用缓存,或发起条件验证请求(如 304 响应复用缓存)。
4.2 CacheStrategy 核心字段与结构
| 字段 | 类型 | 说明 |
|---|---|---|
| request | Request | 用户原始请求(未修改) |
| cacheResponse | @Nullable Response | 可直接返回的缓存响应(过期 but 可用的 stale 响应) |
| networkRequest | @Nullable Request | 需要发起的网络请求(可能附加条件验证头) |
4.3 缓存决策流程(OkHttp 内部实现)
OkHttp 的 CacheStrategy.Factory 根据 RFC 7234 实现以下决策逻辑:
- 检查缓存中是否存在与请求 URI 匹配的响应。
- 如果无缓存 →
networkRequest = request,cacheResponse = null。 - 如果有缓存,判断是否“可重用”:
- 如果请求带
no-cache、no-store、max-age=0等 → 直接跳过缓存,走网络。 - 如果缓存响应未过期(
age < max-age)且没有must-revalidate要求 →cacheResponse有效,networkRequest = null。 - 如果缓存已过期 或 要求必须验证 → 构造条件请求(添加
If-Modified-Since或If-None-Match)赋给networkRequest,同时保留当前缓存作为cacheResponse(304 时复用)。
- 如果请求带
- 返回
CacheStrategy对象,由CacheInterceptor执行。
关键原则:只有当
cacheResponse != null 且 networkRequest == null 时,才会完全从缓存返回;否则至少发起一次网络(可能是条件请求)。
4.4 决策流程图
4.5 核心实现代码(OkHttp 源码简化)
// CacheStrategy.Factory 内部核心逻辑
public CacheStrategy get() {
CacheStrategy strategy = new CacheStrategy(request);
// 1. 查找缓存
Response cacheCandidate = cache.get(request);
if (cacheCandidate == null) {
strategy.networkRequest = request; // 无缓存,必须网络
return strategy;
}
// 2. 检查请求的缓存指令
if (request.noCache() || request.noStore()) {
strategy.networkRequest = request;
return strategy;
}
// 3. 检查缓存的过期情况
long ageMillis = cacheCandidate.ageMillis();
long maxAgeMillis = cacheCandidate.maxAgeMillis();
boolean expired = ageMillis >= maxAgeMillis;
// 4. 检查是否需要重新验证
boolean mustRevalidate = cacheCandidate.mustRevalidate();
if (!expired && !mustRevalidate) {
// 缓存仍然新鲜,可直接使用
strategy.cacheResponse = cacheCandidate;
return strategy;
}
// 5. 缓存过期或需要验证 → 构造条件请求
Request.Builder conditional = request.newBuilder();
String lastModified = cacheCandidate.header("Last-Modified");
if (lastModified != null) {
conditional.header("If-Modified-Since", lastModified);
}
String etag = cacheCandidate.header("ETag");
if (etag != null) {
conditional.header("If-None-Match", etag);
}
strategy.networkRequest = conditional.build();
strategy.cacheResponse = cacheCandidate;
return strategy;
}
4.6 网络与缓存优先级判断规则
| cacheResponse | networkRequest | 最终行为 | 典型场景 |
|---|---|---|---|
| null | 非 null | 强制网络请求 | 无缓存 / 请求含 no-cache |
| 非 null | null | 直接使用缓存,不发起网络 | 缓存未过期、无 must-revalidate |
| 非 null | 非 null | 发起条件请求,若 304 则复用缓存 | 缓存过期 / 必须验证 |
优化建议:在自定义
Interceptor 中提前修改 networkRequest 可灵活控制优先级。例如,强制走网络但依然利用条件验证节省带宽。
4.7 常见错误与避坑
- 忽略
no-cache指令:虽然名称是“不缓存”,但实际含义是“必须验证”,若未正确生成条件请求,可能导致数据陈旧。 - 混淆
max-age=0与no-cache:两者都会强制走网络,但no-cache要求服务器验证,max-age=0仅表示立即过期,验不验证取决于服务器Cache-Control。 - 缓存污染:当
must-revalidate被忽略时,过期缓存被直接返回给客户端,引发数据不一致。 - 条件请求头缺失:若缓存响应无
Last-Modified或ETag,networkRequest退化为完全请求,导致 200 而非 304,此时需要更新缓存。
4.8 性能与稳定性优化
- 优先使用
ETag验证,因为它比Last-Modified更精准,减少误判。 - 对高频请求合理设置
max-age,避免频繁发起条件请求造成服务端压力。 - 在客户端本地存储缓存响应时,保留原始响应头部(尤其
Age、Date、Expires、ETag、Last-Modified),确保CacheStrategy正确计算新鲜度。 - 使用
okhttp3.internal.cache.DiskLruCache时注意文件锁与并发读写,避免缓存损坏导致决策异常。