4. 缓存拦截器原理:拦截器链、CacheInterceptor 源码分析
这一章咱们聊聊 OkHttp 的拦截器链,以及 CacheInterceptor 到底在链条里扮演什么角色。说实话,很多同学用了好几年 Retrofit+OkHttp,却从来没仔细看过这个链条是怎么串起来的。我以前也这样,直到线上出了个缓存不更新的 bug,排查了整整两天……后来才被迫把 OkHttp 的 interceptor 源码翻了个底朝天。嗯,今天就把这些经验嚼碎了喂给你。
4.1 拦截器链:一层一层剥洋葱
OkHttp 的网络请求,本质上就是一条拦截器链。每一层拦截器只关心自己的事,做完就传给下一层。这设计有点像 Unix 的管道,职责单一,组合灵活。
- RetryAndFollowUpInterceptor:负责重试和重定向。我遇到过几次连接超时自动重试后缓存没更新,后来发现这哥们重试时不会通知缓存层——注意,这是坑。
- BridgeInterceptor:把应用层的请求转换成网络请求,设置 Content-Length、Gzip 等。说白了就是帮我们补全 Http 头。
- CacheInterceptor(重点):我们的缓存管理核心。它在 Connect 之前,Network 之后——这个顺序很关键!
- ConnectInterceptor:建立 TCP/SSL 连接。缓存没过期的话,根本轮不到它出场。
- NetworkInterceptors:用户自定义的网络拦截器,只有真正走网络时才触发。
4.2 CacheInterceptor 源码核心方法
直接打开 okhttp3.internal.cache.CacheInterceptor,看看它最核心的 intercept() 方法。我挑关键行讲,代码附在下面。
public Response intercept(Chain chain) throws IOException {
// 1. 尝试从缓存中拿候选响应
Response cacheCandidate = cache != null ? cache.get(chain.request()) : null;
long now = System.currentTimeMillis();
// 2. 构造缓存策略(这就是上讲讲的 CacheStrategy)
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
// 3. 如果不需要网络,且缓存可用
if (networkRequest == null && cacheResponse != null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
// 4. 如果需要网络,继续往下走
Response networkResponse = chain.proceed(networkRequest != null ? networkRequest : chain.request());
// 5. 如果有缓存候选,做条件更新(304 Not Modified)
if (cacheCandidate != null) {
if (networkResponse.code() == HTTP_NOT_MODIFIED) {
Response combined = cacheCandidate.newBuilder()
.headers(networkResponse.headers())
.cacheResponse(stripBody(cacheCandidate))
.networkResponse(stripBody(networkResponse))
.build();
cache.trackConditionalCacheHit();
cache.update(cacheCandidate, combined);
return combined;
}
// 服务端返回新的资源,删除旧缓存
cache.remove(cacheCandidate);
}
// 6. 把新响应写入缓存(如果可缓存)
if (cache != null) {
Response cacheableResponse = cache.put(networkResponse);
if (cacheableResponse != null) {
return cacheableResponse;
}
}
return networkResponse;
}
我来拆解这几个关键步骤:
- 获取缓存候选:先问 Cache 有没有这个请求的响应。注意,这里拿到的是原始副本,也可能为 null。
- 策略裁决:把当前时间、请求、缓存候选交给 CacheStrategy 工厂。它会根据过期时间、max-age、immutable 等参数,决定要不要用缓存。返回的
networkRequest如果为 null,说明缓存尚可用。 - 直接返回缓存:网络请求为 null 且缓存响应非空,直接返回。这就是“缓存命中”最快的路径。
- 发起网络请求:如果不满足,就调用
chain.proceed()继续走 Connect 和 Network 拦截器。 - 条件更新(304):如果服务返回 304 Not Modified,说明缓存还能用,就把新请求的头信息和旧缓存的 body 合并。我见过不少同学没处理这个分支,导致总是重新下载 body——浪费流量。
- 写入新缓存:
cache.put()会把响应存入磁盘,同时根据 Cache-Control 设置过期时间。
stripBody() 是为了避免内存泄漏——它把响应体替换成空体。但如果你直接操作 response.body(),后面再用就可能报错。我曾经在自定义拦截器中想二次读取缓存响应体,结果抛了“closed”异常,排查半天发现是 stripBody 搞的鬼。
4.3 拦截器链的执行顺序为什么重要?
你看上图,CacheInterceptor 位于 ConnectInterceptor 之前。这设计很巧妙:
如果缓存命中,直接返回,根本不需要建立网络连接。但如果缓存策略允许使用过期缓存(比如 stale-while-revalidate),它会在后台发起异步网络请求来更新缓存,同时先用旧缓存响应用户。这种“幕后刷新”的机制,正是 OkHttp 比很多其他 HTTP 库高明的地方。
| 拦截器名 | 位置 | 缓存相关行为 |
|---|---|---|
| RetryAndFollowUp | 链首 | 重试时不会主动清缓存,需自行处理 |
| Bridge | 第二 | 添加 Cache-Control 头部,影响策略 |
| Cache | 第三 | 拦截网络、返回缓存、更新缓存 |
| Connect | 第四 | 缓存未命中时才执行 |
| Network | 第五 | 用户自定义,可修改响应内容 |
缓存策略的最终决策权在 CacheStrategy,而 CacheInterceptor 只是执行者。想调优缓存,要么改请求头(Cache-Control),要么自定义 Cache 实现,要么在拦截器链中“插队”提前干预。
4.4 个人遇到的一个典型坑
有一次我们 App 的图片列表加载很慢,查了发现每次请求都走了网络。看日志发现 Cache-Interceptor 返回的 cacheResponse 始终为 null。问题出在哪?我们服务端返回的响应头没有 Cache-Control 也没有 Last-Modified。OkHttp 对不可缓存的响应默认不存缓存。
当时我写了一行代码在应用层添加默认缓存头:
// 在应用拦截器中注入默认缓存策略
request.newBuilder()
.header("Cache-Control", "public, max-age=300")
.build();
但注意:不能写死在所有请求上,否则更新接口也会被缓存。后来我改成只对 GET 请求添加,加了个白名单。嗯,这种细节不趟一次坑是真不知道。