缓存策略对象(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 转换流程

下图展示了从原始请求 + 缓存条目到策略决策的完整路径:

CacheStrategy 转换逻辑(CacheStrategy.Factory.create) Request + CacheEntry CacheStrategy.Factory networkRequest cacheResponse networkRequest == null ? (缓存足够新鲜) 直接返回缓存 cacheResponse == null ? (没有可用缓存) 发起完整网络请求 带条件头网络请求 (If-None-Match等) Strategy 对象三个字段决定了拦截器的后续行为 · networkRequest=null, cacheResponse!=null → 直接使用缓存 · networkRequest!=null, cacheResponse=null → 发起完整网络请求 · 两者都不为null → 发送带条件头的请求,服务端根据304决定缓存是否有效

4.3 核心 API 与实现流程

OkHttp 在 CacheInterceptor 中调用 CacheStrategy.Factory.create() 生成策略对象。关键步骤:

  1. 从缓存中根据请求 URL 和缓存键获取 CacheEntry(包含原始响应、过期时间等)。
  2. 调用 new CacheStrategy.Factory(request, cacheResponse) 构造工厂。
  3. 工厂内部解析请求和缓存的 Cache-Control、Expires、Last-Modified 等字段,计算出 networkRequest 和 cacheResponse。
  4. 返回不可变的 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 时需要用其组装最终响应。