4. CacheStrategy 缓存决策器:请求与响应的缓存策略生成

4.1 缓存决策器的作用

  • 策略生成:根据原始请求的 Cache-Control、If-Modified-Since、If-None-Match 等头部以及存储的缓存响应,生成两种候选策略:
    ① 使用缓存(cacheResponse 不为 null)
    ② 发起网络请求(networkRequest 不为 null)。
  • 优先级判断:当 cacheResponse 和 networkRequest 同时存在时,由调用方(例如 Interceptor)决定优先走网络还是缓存。
  • 决策结果:最终决定本次请求是否使用缓存,或发起条件验证请求(如 304 响应复用缓存)。

4.2 CacheStrategy 核心字段与结构

字段类型说明
requestRequest用户原始请求(未修改)
cacheResponse@Nullable Response可直接返回的缓存响应(过期 but 可用的 stale 响应)
networkRequest@Nullable Request需要发起的网络请求(可能附加条件验证头)

4.3 缓存决策流程(OkHttp 内部实现)

OkHttp 的 CacheStrategy.Factory 根据 RFC 7234 实现以下决策逻辑:

  1. 检查缓存中是否存在与请求 URI 匹配的响应。
  2. 如果无缓存 → networkRequest = request,cacheResponse = null。
  3. 如果有缓存,判断是否“可重用”:
    • 如果请求带 no-cache、no-store、max-age=0 等 → 直接跳过缓存,走网络。
    • 如果缓存响应未过期(age < max-age)且没有 must-revalidate 要求 → cacheResponse 有效,networkRequest = null。
    • 如果缓存已过期 或 要求必须验证 → 构造条件请求(添加 If-Modified-Since 或 If-None-Match)赋给 networkRequest,同时保留当前缓存作为 cacheResponse(304 时复用)。
  4. 返回 CacheStrategy 对象,由 CacheInterceptor 执行。
关键原则:只有当 cacheResponse != null 且 networkRequest == null 时,才会完全从缓存返回;否则至少发起一次网络(可能是条件请求)。

4.4 决策流程图

CacheStrategy 缓存决策流程 开始决策 检查缓存存在? 无缓存 → 直接网络请求 缓存是否有效? 有效 → 直接使用缓存 过期/需验证 构造条件请求 (If-None-Match / If-Modified-Since) 响应 304? 使用缓存+更新头 使用新响应 优先判断原则 networkRequest == null → 仅缓存 networkRequest != null → 走网络

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 网络与缓存优先级判断规则

cacheResponsenetworkRequest最终行为典型场景
null非 null强制网络请求无缓存 / 请求含 no-cache
非 nullnull直接使用缓存,不发起网络缓存未过期、无 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 时注意文件锁与并发读写,避免缓存损坏导致决策异常。