第1章 拦截器基础与缓存核心原理
在 OkHttp 中,拦截器(Interceptor)是整个请求生命周期的核心扩展点。它允许你在请求发送前、响应返回后、以及重试与重定向过程中插入自定义逻辑。理解拦截器链的运转机制以及缓存策略的优先级,是高效配置 OkHttp 缓存的根基。
1.1 拦截器类型:Application vs Network
OkHttp 内置两条拦截器链,它们在请求生命周期中的位置和可见性不同。下表总结了核心差异:
| 维度 | Application Interceptor | Network Interceptor |
|---|---|---|
| 注册方式 | addInterceptor() |
addNetworkInterceptor() |
| 触发时机 | 缓存命中前、重试和重定向都会重新进入 | 只在真正发起网络请求时执行(重定向、重试时不会重复) |
| 可见范围 | 可观察到 curl 级别的原始请求/响应(包括重定向结果) | 可观察到一次实际网络请求的中间结果 |
| 适用场景 | 日志、Header 注入、统一认证、缓存控制 | 网络重定向跟踪、自定义 DNS、连接池监控 |
⚠️ 常见误区: 许多人将缓存相关逻辑只放在 Network Interceptor 中,导致缓存命中时 Network 拦截器不会被调用,容易遗漏条件请求头处理。缓存拦截通常应放在 Application 层级。
1.2 缓存机制:Cache-Control 与缓存策略
OkHttp 的缓存行为由 HTTP 响应头 Cache-Control 主导,同时遵循强制缓存和协商缓存两个层级。
1.2.1 强制缓存(Cache-Control: max-age)
在有效期内,客户端直接使用本地缓存,不与服务器通信。
max-age=N:从请求时刻起,N 秒内直接使用缓存。immutable:暗示不会更新,常用于静态资源。no-cache:不允许强制缓存,必须与服务器协商。no-store:禁止所有缓存。
1.2.2 协商缓存(ETag / Last-Modified)
强制缓存失效后,客户端携带条件请求头询问服务器资源是否变化:
If-None-Match:与服务器 ETag 比较,匹配则返回 304 Not Modified。If-Modified-Since:与服务器 Last-Modified 比较,返回 304 或新资源。
OkHttp 内部自动处理 304 响应,用缓存副本更新并返回给上层。
1.2.3 缓存策略优先级(从高到低)
- Cache-Control: no-store —— 禁用缓存
- Cache-Control: no-cache —— 强制协商
- Cache-Control: max-age 或
Expires—— 强制缓存(Expires 优先级低于 max-age) - ETag / Last-Modified —— 协商缓存(ETag 优先于 Last-Modified)
- 用户自定义 / 请求端 Cache-Control —— 客户端可通过
CacheControl.FORCE_NETWORK或CacheControl.FORCE_CACHE覆盖
1.3 实现流程:开启缓存并配置拦截器
以下代码展示如何在 OkHttp 中开启缓存并注册 Application 拦截器:
// 1. 创建缓存目录(通常放在应用专属缓存目录)
File cacheDir = new File(getCacheDir(), "okhttp_cache");
int cacheSize = 50 * 1024 * 1024; // 50 MB
// 2. 构建 Cache 对象
Cache cache = new Cache(cacheDir, cacheSize);
// 3. 自定义 Application 拦截器(用于日志 / 缓存策略)
Interceptor appInterceptor = chain -> {
Request request = chain.request();
// 示例:为请求添加 Cache-Control 头(强制使用缓存)
Request newRequest = request.newBuilder()
.cacheControl(CacheControl.FORCE_CACHE)
.build();
Response response = chain.proceed(newRequest);
// 日志输出
System.out.println("Response code: " + response.code());
return response;
};
// 4. 构建 OkHttpClient
OkHttpClient client = new OkHttpClient.Builder()
.cache(cache)
.addInterceptor(appInterceptor) // Application 层级
.addNetworkInterceptor(interceptor2) // Network 层级(可选)
.build();
1.4 常见错误与调试要点
- 缓存目录未创建:
getCacheDir()返回的目录不存在时会抛出IOException,建议先执行mkdirs()。 - 拦截器顺序错误: 在 Application 拦截器中修改请求头后,Network 拦截器可能收到修改后的请求;但如果同时使用了
addInterceptor和addNetworkInterceptor,注意响应经过的链顺序。 - 缓存未生效: 检查服务器响应是否包含
Cache-Control头,以及客户端是否正确配置了Cache对象。 - 调试技巧: 打印
response.cacheResponse()和response.networkResponse()可观察缓存来源。
// 调试示例:打印缓存命中信息
Response response = client.newCall(request).execute();
if (response.cacheResponse() != null) {
System.out.println("Response from CACHE: " + response.cacheResponse().code());
} else if (response.networkResponse() != null) {
System.out.println("Response from NETWORK: " + response.networkResponse().code());
}
📌 性能优化提示: 强制缓存可大幅降低网络开销,但需合理设置
max-age 值。对于动态 API,建议使用 no-cache + ETag 组合,既保证及时更新又减少带宽消耗。
1.5 部署时注意事项
- 生产环境中缓存目录应使用应用私有目录,避免被系统清理。
- 缓存大小应根据设备可用空间调整,一般设为请求资源总大小的 2~3 倍。
- 若使用
CacheControl.FORCE_CACHE且无有效缓存,请求将直接失败,此时应先检查是否存在缓存。