第2章 · Retrofit缓存拦截器架构:OkHttp Cache、Interceptor、NetworkInterceptor
2.1 架构原理
Retrofit 本身不直接处理缓存,而是委托给底层的 OkHttp 客户端。OkHttp 提供了一套三层拦截器体系,用于控制请求与响应的缓存行为:
- Interceptor(应用拦截器):在请求发送前和响应返回后执行,位于整个请求链的最外层,适合注入通用缓存头或日志。
- OkHttp Cache(缓存存储层):基于磁盘的缓存策略,根据 RFC 规范自动判断缓存有效性。
- NetworkInterceptor(网络拦截器):在真正发起网络请求前和收到原始响应后执行,位于缓存层之后,可精确控制条件请求(ETag / Last-Modified)。
核心原则:Interceptor 对缓存的数据不可见,而 NetworkInterceptor 可以看到缓存未命中时的原始网络响应,因此更适用于缓存头校验逻辑。
2.2 核心组件职责对比
| 组件 | 执行位置 | 可见范围 | 典型用途 |
|---|---|---|---|
| Interceptor | 缓存策略之前 | 应用层请求/响应 | 注入通用 Cache-Control、日志记录、重试逻辑 |
| OkHttp Cache | 拦截器链中间 | 磁盘缓存存储与命中判断 | 根据 RFC 标准自动管理缓存有效期 |
| NetworkInterceptor | 缓存策略之后 | 原始网络请求/响应 | 处理 ETag、Last-Modified 条件请求 |
2.3 实现流程:构建带缓存的 Retrofit 实例
以下步骤展示了如何将三层拦截器与 Retrofit 整合:
- 创建 OkHttp Cache 实例,指定缓存目录与最大大小。
- 添加应用层 Interceptor 注入通用缓存头。
- 添加网络层 NetworkInterceptor 实现条件缓存校验。
- 构建 OkHttpClient 并传入 Retrofit.Builder。
// 步骤1:定义缓存目录与大小
File cacheDir = new File(context.getCacheDir(), "http_cache");
long cacheSize = 10 * 1024 * 1024; // 10 MB
Cache okHttpCache = new Cache(cacheDir, cacheSize);
// 步骤2:应用拦截器 — 注入通用缓存头
Interceptor appInterceptor = chain -> {
Request original = chain.request();
Request modified = original.newBuilder()
.header("Cache-Control", "max-age=60")
.build();
return chain.proceed(modified);
};
// 步骤3:网络拦截器 — 条件缓存
Interceptor networkInterceptor = chain -> {
Response originalResponse = chain.proceed(chain.request());
return originalResponse.newBuilder()
.header("Cache-Control", "public, max-age=300")
.removeHeader("Pragma")
.build();
};
// 步骤4:构建 OkHttpClient
OkHttpClient client = new OkHttpClient.Builder()
.cache(okHttpCache)
.addInterceptor(appInterceptor)
.addNetworkInterceptor(networkInterceptor)
.build();
// 步骤5:构建 Retrofit 实例
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build();
2.4 调试与验证
通过日志或抓包工具验证拦截器是否生效:
// 应用拦截器日志示例(每次请求均打印)
D/OkHttp: --> GET /users/list HTTP/1.1
D/OkHttp: --> Cache-Control: max-age=60
D/OkHttp: --> END GET
// 缓存命中时,NetworkInterceptor 不会触发
D/OkHttp: <-- 200 OK (cache) // ← 无 networkInterceptor 日志
// 缓存未命中时,NetworkInterceptor 生效
D/OkHttp: --> GET /users/list HTTP/1.1
D/OkHttp: --> If-None-Match: "abc123"
D/OkHttp: <-- 304 Not Modified
D/OkHttp: --> Cache-Control: public, max-age=300
调试建议:使用 OkHttp 的日志拦截器(HttpLoggingInterceptor)分别添加到 Interceptor 和 NetworkInterceptor 位置,可以清晰看到两层拦截器的调用时机差异。
2.5 常见错误与避坑
| 错误场景 | 原因 | 解决方案 |
|---|---|---|
| 缓存始终不生效 | 服务器未返回 Cache-Control 头 | 在 NetworkInterceptor 中手动添加缓存头 |
| Interceptor 中修改了缓存数据 | 误操作 Response body 导致缓存损坏 | 只修改 header,不触碰 body |
| NetworkInterceptor 不执行 | 缓存命中后直接返回,不触发网络层 | 确认缓存策略无强制缓存,或用 F12 清除缓存后重试 |
| 磁盘缓存写入失败 | 缓存目录不可写或权限不足 | 使用 Context.getCacheDir() 确保路径合法 |
2.6 性能与稳定性优化
- 缓存大小控制:根据业务场景设置合理的 cacheSize(通常 5~50 MB),避免磁盘频繁 GC。
- 拦截器顺序:通用头注入放在 Interceptor,条件校验放在 NetworkInterceptor,职责分离。
- 避免在 Interceptor 中做耗时操作:拦截器运行在 OkHttp 的调度线程,长时间阻塞会耗尽连接池。
- 配合 Stetho 或 Chucker:可视化查看缓存命中状态与响应头,加速调试。
- 缓存目录隔离:不同业务模块使用独立的 Cache 实例,防止缓存污染。
⚠ 稳定性风险:不要在 Interceptor 或 NetworkInterceptor 中直接抛出未捕获的异常,会导致整个请求链路中断。建议使用 try-catch 包装后返回 fallback 响应或重试。
2.7 架构决策总结
选择 Interceptor 还是 NetworkInterceptor 取决于你的缓存策略目标:
- 需要全局覆盖缓存头 → 用
addInterceptor()注入通用 Cache-Control。 - 需要精细条件缓存(ETag / Last-Modified) → 用
addNetworkInterceptor()。 - 需要同时使用 → 按上述流程分层配置,互不冲突。