第2章 缓存拦截器与责任链机制
OkHttp 的缓存能力并非孤立的模块,而是通过 责任链(Chain of Responsibility)设计模式,以拦截器链的方式织入请求-响应全流程。CacheInterceptor 正是这条链上负责缓存读写、校验、更新的核心节点。理解它在链中的位置与传递流程,是掌握整个缓存机制的第一步。
本章目标
✓ 掌握 OkHttp 拦截器链的整体拓扑
✓ 理解 CacheInterceptor 在链中的上下游关系
✓ 厘清请求/响应在缓存节点上的完整传递顺序
✓ 掌握责任链模式下各拦截器的职责隔离
✓ 掌握 OkHttp 拦截器链的整体拓扑
✓ 理解 CacheInterceptor 在链中的上下游关系
✓ 厘清请求/响应在缓存节点上的完整传递顺序
✓ 掌握责任链模式下各拦截器的职责隔离
2.1 OkHttp 拦截器链概览
OkHttp 将一次网络请求拆解为多个拦截器(Interceptor),每个拦截器只处理自己关心的逻辑,然后通过 chain.proceed() 将请求传递给下一个拦截器,最终获得响应后再逐层返回。这种管道式架构使得缓存、重定向、重试、连接复用、数据转换等能力可以被灵活组合。
| 拦截器类 | 职责 | 执行顺序 |
|---|---|---|
RetryAndFollowUpInterceptor |
失败重试、重定向跟踪 | 第1个(应用层首入) |
BridgeInterceptor |
请求头转换(Cookie、Gzip、Content-Length) | 第2个 |
CacheInterceptor |
缓存读取、条件更新、缓存写入 | 第3个 |
ConnectInterceptor |
建立 TCP/TLS 连接 | 第4个 |
CallServerInterceptor |
发送 HTTP 请求并读取原始响应 | 第5个(最内层) |
关键约定:拦截器链的“内外”概念——越靠前的拦截器越“外层”,越靠后的拦截器越“内层”。
CacheInterceptor 位于中间位置,恰好在网络连接建立之前,这是它能拦截网络请求、先查缓存的关键。
2.2 责任链中 CacheInterceptor 的位置与传递流程
下面的流程图清晰展示了 OkHttp 默认拦截器链中 CacheInterceptor 的插入位置以及请求/响应在链上的完整传递路径。
2.3 CacheInterceptor 内部核心流程
当请求到达 CacheInterceptor 时,拦截器会按以下步骤决策:
- 读取缓存候选(Cache Candidate):调用
InternalCache.get(request),尝试从磁盘或内存缓存中获取之前缓存的CacheResponse。 - 判断缓存是否可用:根据
CacheStrategy计算规则(结合 max-age、must-revalidate 等指令)判断缓存是否新鲜。若新鲜则直接返回缓存响应,不再继续向下传递请求(短路返回)。 - 构建条件请求:若不新鲜,在原始请求中添加
If-None-Match(ETag)和If-Modified-Since(Last-Modified)头部,构造条件验证请求。 - 传递请求给下一个拦截器:调用
chain.proceed(conditionalRequest),将条件请求交给ConnectInterceptor和CallServerInterceptor发送到服务端。 - 处理网络响应:如果服务端返回
304 Not Modified,则复用缓存数据,返回缓存的响应体;如果返回200 OK,则使用新响应,并将新响应写入缓存。 - 缓存写入:对于可缓存的 200 响应,通过
InternalCache.put(response)更新缓存,并返回新响应给上一级拦截器。
2.4 关键代码:CacheInterceptor.intercept 骨架
@Override
public Response intercept(Chain chain) throws IOException {
// 1. 尝试从缓存中获取候选响应
Response cacheCandidate = cache != null ? cache.get(chain.request()) : null;
// 2. 计算缓存策略
CacheStrategy strategy = new CacheStrategy.Factory(System.currentTimeMillis(),
chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
// 3. 缓存命中且有效:直接返回缓存,不发起网络请求
if (cacheResponse != null && networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
// 4. 发起网络请求(携带条件验证头部)
Response networkResponse = null;
try {
networkResponse = chain.proceed(networkRequest != null ? networkRequest : chain.request());
} finally {
// 如果缓存是非空但有条件验证,清理旧缓存
if (cacheResponse != null && cacheCandidate == null) {
closeQuietly(cacheResponse.body());
}
}
// 5. 处理网络响应:304 复用缓存,200 更新缓存
if (cacheResponse != null && networkResponse.code() == HTTP_NOT_MODIFIED) {
Response response = cacheResponse.newBuilder()
.headers(networkResponse.headers())
.sentRequestAtMillis(networkResponse.sentRequestAtMillis())
.receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
response.body().close();
cache.trackConditionalCacheHit();
cache.update(cacheResponse, response);
return response;
} else {
// 新响应,写入缓存
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
if (cache != null && HttpHeaders.hasBody(response) && cacheStrategy.isCacheable(response, networkRequest)) {
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
return response;
}
}
常见错误:初学者容易混淆
•
• 两者均非 null → 需要发送条件请求验证缓存是否过期。
错误的条件判断会导致缓存击穿或重复网络请求。
networkRequest 和 cacheResponse 的 null 判断逻辑。务必理解 CacheStrategy 输出的两种可能:•
networkRequest == null 且 cacheResponse != null → 缓存新鲜,直接返回缓存。• 两者均非 null → 需要发送条件请求验证缓存是否过期。
错误的条件判断会导致缓存击穿或重复网络请求。
2.5 调试与验证方法
在开发环境中,可以通过以下方式验证 CacheInterceptor 是否按预期工作:
- 启用 OkHttp 日志拦截器:在拦截器链中添加
HttpLoggingInterceptor,并设置为BODY级别,可以观察到条件请求头部(If-None-Match)是否被正确添加,以及服务端响应状态码是304还是200。 - 设置断点:在
CacheInterceptor.intercept()的 cacheResponse 和 networkResponse 分支上打断点,观察CacheStrategy的决策结果。 - 检查缓存文件:在 Android 中使用
adb shell ls -l /data/data/<包名>/cache/okhttp/查看缓存目录下的条目数及文件大小。
// 日志拦截器配置示例
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.HEADERS);
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(logging) // 应用层拦截器,优先执行
.cache(cache) // 必须设置 cache,否则 CacheInterceptor 会跳过
.build();
2.6 性能与稳定性优化建议
| 关注点 | 建议措施 | 影响 |
|---|---|---|
| 缓存读 I/O 延迟 | 使用 CacheControl.FORCE_CACHE 或增大 max-stale 减少网络请求 |
提升弱网环境响应速度,但可能返回陈旧数据 |
| 缓存写入频率 | 对 304 响应仅更新头部,避免重复写入 body | 减少磁盘写放大,延长闪存寿命 |
| 并发请求冲突 | 确保 InternalCache 实现是线程安全的(OkHttp 内置的 DiskLruCache 已处理) |
避免缓存损坏或 OOM |
| 条件请求风暴 | 合理设置 max-stale 和 must-revalidate,避免大量资源同时发起条件验证 | 减轻服务端压力,降低带宽消耗 |
2.7 常见问题排查
问题 1:已设置 cache,但始终不走缓存
原因:
解决:检查请求和响应中的 Cache-Control 头部;确认服务端返回了正确的缓存相关头部(如
原因:
CacheControl 指令中指定了 no-cache 或 no-store,导致 CacheStrategy 判定 networkRequest 始终不为 null。解决:检查请求和响应中的 Cache-Control 头部;确认服务端返回了正确的缓存相关头部(如
Cache-Control: max-age=3600)。
问题 2:条件请求无效,每次返回 200 而非常 304
原因:服务端不支持 ETag/Last-Modified,或者客户端发送的
解决:使用日志拦截器打印请求头,确认条件头部是否被正确添加;检查服务端响应是否包含
注意:OkHttp 的 CacheInterceptor 默认只在首次缓存后缓存 ETag/LM,若服务端未返回,则条件验证将退化。
原因:服务端不支持 ETag/Last-Modified,或者客户端发送的
If-None-Match 值与服务端当前 ETag 不匹配。解决:使用日志拦截器打印请求头,确认条件头部是否被正确添加;检查服务端响应是否包含
ETag 或 Last-Modified。注意:OkHttp 的 CacheInterceptor 默认只在首次缓存后缓存 ETag/LM,若服务端未返回,则条件验证将退化。
2.8 小结:责任链设计的好处
将缓存逻辑封装在 CacheInterceptor 中,并通过责任链与网络层解耦,带来了以下优势:
- 单一职责:每个拦截器只关心自己的事,缓存不会侵入连接复用或请求重试逻辑。
- 可扩展性:开发者可以自定义拦截器插入链中,实现自定义缓存策略(如内存+磁盘二级缓存)。
- 可测试性:可以单独为 CacheInterceptor 编写单元测试,模拟 Chain 并验证缓存决策。
- 短路优化:缓存命中时直接返回,避免网络 I/O 开销,这是责任链模式最实用的性能收益。