1. OkHttp 缓存整体架构与核心类

1.1 缓存定位与设计目标

OkHttp 的缓存系统内嵌于拦截器链中,位于网络请求与服务器之间。其核心目标是在满足 HTTP 缓存语义的前提下,通过本地磁盘复用响应数据,减少网络请求次数、降低延迟、节省带宽。

整个缓存体系由四个核心类协作驱动:

  • Cache:负责响应数据的磁盘存储与读取管理。
  • CacheStrategy:根据请求头与缓存响应头计算缓存是否可用、是否需要网络。
  • Interceptor:CacheInterceptor 作为缓存决策的执行入口,串联 Cache 与 CacheStrategy。
  • Response 缓存头:服务端通过 Cache-Control、Expires、ETag、Last-Modified 等头字段控制缓存行为。

1.2 核心类职责对比

核心类 职责 关键 API / 字段
Cache 磁盘缓存的读写管理,基于 DiskLruCache 实现 Cache(File directory, long maxSize), get(Request), put(Response), remove(Request)
CacheStrategy 根据请求与缓存响应生成决策:使用缓存、网络请求或两者结合 Factory.compute(CacheControl, CacheControl),输出 networkRequest 与 cacheResponse
CacheInterceptor OkHttp 拦截器链中的缓存处理节点,执行策略并写入缓存 intercept(Chain chain) 中调用 CacheStrategy 并维护 Response 封闭性
Response 缓存头 服务端通过 HTTP 头字段声明缓存策略 Cache-Control: max-age, no-cache, no-store, must-revalidate 等

1.3 整体架构交互流程

用户发起请求后,OkHttp 将请求依次通过拦截器链。当请求到达 CacheInterceptor 时,缓存系统按以下顺序协作:

  1. 从 Cache 中读取候选响应:根据请求的 URL、请求头等条件,从磁盘缓存中查找匹配的 Response。
  2. 构建 CacheStrategy:将原始请求与缓存响应送入 CacheStrategy.Factory,计算当前缓存是否有效、是否需要携带条件请求头(如 If-None-Match)重验证。
  3. 执行决策:根据 CacheStrategy 输出的 networkRequest 和 cacheResponse,决定直接返回缓存、发起网络请求或组合使用。
  4. 更新 Cache:若网络请求后得到新响应,CacheInterceptor 将其写入 Cache;若服务器返回 304 Not Modified,则合并缓存响应头并返回。
OkHttp 缓存决策核心流程 发起请求 CacheInterceptor Cache.get(Request) CacheStrategy.compute() 决策分支 直接返回缓存 发起网络 / 条件请求 CacheStrategy 根据缓存头与请求头计算后续操作,决定走“直接返回”还是“网络请求”分支

1.4 代码配置与初始化

启用缓存的第一步是创建 Cache 对象并附加到 OkHttpClient.Builder:

File cacheDir = new File(context.getCacheDir(), "okhttp_cache");
long cacheSize = 10 * 1024 * 1024; // 10 MB
Cache cache = new Cache(cacheDir, cacheSize);

OkHttpClient client = new OkHttpClient.Builder()
    .cache(cache)
    .addNetworkInterceptor(new CacheInterceptor()) // 内部已内置,此处仅为示意
    .build();
关键点: Cache 构造函数第二个参数 maxSize 是软上限,OkHttp 会在后台线程中执行 LRU 清理,当磁盘缓存超出此大小时自动删除最旧条目。

1.5 调试缓存行为

通过添加日志拦截器或利用 Response.cacheResponse() 与 Response.networkResponse() 可以观察缓存命中状态:

Response response = client.newCall(request).execute();
if (response.cacheResponse() != null) {
    System.out.println("缓存命中: " + response.cacheResponse().code());
} else if (response.networkResponse() != null) {
    System.out.println("网络请求: " + response.networkResponse().code());
}

开启 OkHttp 日志框架(如 HttpLoggingInterceptor)可查看详细缓存头交换:

HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.HEADERS);
OkHttpClient client = new OkHttpClient.Builder()
    .addInterceptor(logging)
    .cache(cache)
    .build();

1.6 常见错误

  • 未设置缓存目录权限:Cache 初始化时若目录不可写,会抛出 IOException,导致整个客户端构建失败。
  • 缓存大小过小:maxSize 设置太小(如 <1MB)会导致频繁的 LRU 清理,实际缓存命中率极低。
  • 混淆强制缓存与网络缓存:Cache-Control: no-cache 并非禁用缓存,而是要求每次使用前向服务器验证;no-store 才是彻底禁止缓存。
  • 忽略 CacheStrategy 的 null 检查:CacheStrategy.networkRequest 与 CacheStrategy.cacheResponse 可能为 null,自定义拦截器若未判空会触发空指针。

1.7 性能与稳定性优化

  • 缓存目录选择:使用 context.getCacheDir() 而非外部存储,避免权限问题与系统自动清理冲突。
  • 合理设置 maxSize:根据应用资源类型计算,图片密集型应用建议 20~50 MB,API 数据型 5~10 MB。
  • 使用 Control-Cache 头细粒度控制:服务端返回 max-age=3600, stale-while-revalidate=300 可在缓存过期后仍提供旧数据,同时后台异步刷新。
  • 离线场景降级:结合 CacheStrategy 的 onlyIfCached 控制,在网络不可用时强制只返回缓存,避免请求挂起。

本章核心摘要:

  • Cache 负责磁盘存储,CacheStrategy 负责决策,CacheInterceptor 执行决策并维护状态。
  • 响应缓存头是服务端策略的载体,Cache-Control 字段决定了缓存的存活时间与验证规则。
  • 正确初始化 Cache 并利用日志分析缓存行为,是后续章节离线拦截实践的基础。