3. 拦截器链中的缓存拦截器:CacheInterceptor 源码分析与策略抉择
在 OkHttp 的拦截器链中,CacheInterceptor 扮演着守门人的角色。它位于 BridgeInterceptor(负责处理请求头/自动压缩)之后,ConnectInterceptor(负责建立 TCP/TLS 连接)之前。它的核心职责是根据预设的缓存策略,决定是直接返回缓存的响应、发起一次条件 GET(带 ETag/If-None-Match)请求,还是完全忽略缓存、直接发送完整请求到服务器。
本章将从源码层面剖析 CacheInterceptor 的主流程,深入理解其缓存命中与写入的决策机制,并掌握如何通过配置 CacheStrategy 来精确控制网络与缓存策略的选择。
3.1 核心源码分析:intercept() 方法的完整闭环
CacheInterceptor 的 intercept() 方法实现了缓存的核心闭环。以下代码展示了其骨架逻辑:
@Override
public Response intercept(Chain chain) throws IOException {
// 1. 尝试从缓存中获取候选响应
Response cacheCandidate = cache != null ? cache.get(request) : null;
long now = System.currentTimeMillis();
// 2. 利用 CacheStrategy 工厂生成当前请求的策略
CacheStrategy strategy = new CacheStrategy.Factory(now, request, cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
// 3. 策略禁止网络且无缓存 -> 返回 504 Unsatisfiable Request
if (networkRequest == null && cacheResponse == null) {
return new Response.Builder()
.code(504)
.message("Unsatisfiable Request (only-if-cached)")
...
.build();
}
// 4. 策略禁止网络但有缓存 -> 直接返回缓存响应
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
// 5. 策略允许网络 -> 发起实际网络请求
Response networkResponse = null;
try {
networkResponse = chain.proceed(networkRequest);
} catch (IOException e) {
// 6. 网络请求失败,但有缓存回退 -> 使用过期缓存(离线容灾)
if (cacheResponse != null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
throw e;
}
// 7. 缓存可用且网络返回 304 -> 使用缓存响应,合并网络头部
if (cacheResponse != null) {
if (networkResponse.code() == HTTP_NOT_MODIFIED) {
Response response = cacheResponse.newBuilder()
.headers(networkResponse.headers())
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
...
return response;
}
}
// 8. 写入缓存(只有当响应可缓存时)
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
if (cache != null) {
if (response.cacheControl().noStore() || ...) {
cache.put(response);
}
}
return response;
}
3.2 缓存命中与写入时机
缓存的命中与写入完全由 CacheStrategy 控制,它遵循 RFC 7234 规范。以下表格展示了常见的 Cache-Control 指令如何影响决策:
| 场景 | 请求/响应指令 | CacheStrategy 结果 | 实际网络行为 |
|---|---|---|---|
| 强制缓存 | max-age=3600 (Response) |
仅使用缓存 | 直接返回缓存的响应体,不发起任何网络请求。 |
| 条件验证 | no-cache 或 携带 ETag |
条件 GET | 发起带 If-None-Match 头的请求。304 则合并头部并复用缓存体,200 则更新缓存。 |
| 禁止存储 | no-store (Response) |
仅网络 | 忽略所有缓存逻辑,直接发起完整网络请求,不写入缓存。 |
| 仅限缓存 | only-if-cached (Request) |
仅缓存 / 504 | 缓存未命中则立即返回 504 Unsatisfiable Request,不发起网络请求。 |
写入时机核心原则: 只有当 networkRequest 不为 null(即实际发出了网络请求),且返回的 networkResponse 是可缓存的状态码(通常为 2xx、3xx),并且响应头中不包含 no-store 指令时,CacheInterceptor 才会在返回响应前将结果写入 DiskLruCache。如果响应包含 private 指令,缓存会正常写入,但共享缓存不得存储。
3.3 网络与缓存策略选择:CacheStrategy 的深度工作流
CacheStrategy 内部类通过工厂方法 get() 计算决策。其核心算法会计算缓存响应的当前年龄(ageMillis),并与新鲜度生命周期(freshnessLifespan)进行对比:
- 年龄计算:
ageMillis = (receivedTimestamp - sentTimestamp) + (ageFromHeader ?: 0)。 - 新鲜度计算:
freshnessLifespan = maxAge != -1 ? maxAge : (expiresTimestamp - dateValue)。 - 决策逻辑: 如果 ageMillis 小于 freshnessLifespan,且没有验证器要求重新验证,则直接使用缓存。如果 ageMillis 超过新鲜度,但存在
ETag或Last-Modified,则生成条件 GET 请求。
// CacheStrategy.Factory 中的核心计算逻辑
if (ageMillis < freshnessLifespan) {
// 1. 缓存仍然是新鲜的,直接使用
strategy.networkRequest = null;
strategy.cacheResponse = cacheResponse;
} else if (hasValidator(cacheResponse)) {
// 2. 缓存已过期,但存在验证器(ETag/Last-Modified),执行条件 GET
Request.Builder conditionalRequest = request.newBuilder();
if (etag != null) {
conditionalRequest.header("If-None-Match", etag);
}
strategy.networkRequest = conditionalRequest.build();
strategy.cacheResponse = cacheResponse;
} else {
// 3. 缓存已过期且无验证器,直接发起网络请求
strategy.networkRequest = request;
strategy.cacheResponse = null;
}
3.4 调试、常见错误与性能优化
调试技巧: 通过添加自定义 Interceptor 可以清晰观察到 CacheInterceptor 的执行结果。
// 添加日志拦截器观察缓存行为
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(chain -> {
Request request = chain.request();
Log.d("CacheDemo", "请求 URL: " + request.url());
Log.d("CacheDemo", "请求头: " + request.headers());
Response response = chain.proceed(request);
// 关键:检查 cacheResponse 和 networkResponse 是否为 null
Log.d("CacheDemo", "是否来自缓存: " + (response.cacheResponse() != null));
Log.d("CacheDemo", "是否来自网络: " + (response.networkResponse() != null));
return response;
})
.cache(new Cache(new File(context.getCacheDir(), "http_cache"), 10 * 1024 * 1024))
.build();
常见错误陷阱:
- 禁用缓存而非验证: 在处理用户信息变更时,许多开发者使用
no-cache而非must-revalidate,导致每次都浪费带宽重新下载完整内容。 - 忘记设置 Cache 目录: 只添加了
Cache-Control头但未使用OkHttpClient.cache()配置Cache对象,导致拦截器永远无法从磁盘命中缓存。 - 静态资源不缓存: 动态 API 响应未设置
max-age或ETag,导致每次页面切换都需要重复请求相同的资源数据。
性能优化建议: 利用 CacheStrategy 实现离线优先架构。对于文档型列表页,建议设置 max-age=60, stale-while-revalidate=86400,这样可以在缓存过期后仍然立即展示旧数据,同时在后台静默更新,极大提升应用启动速度与内容加载感知。