4. 自定义缓存拦截器设计:实现 Interceptor 接口、请求/响应拦截、动态控制缓存策略入口
4.1 原理概述
OkHttp 的 Interceptor 接口允许开发者在请求链中插入自定义逻辑。缓存拦截器位于应用拦截器(Application Interceptor)之后、网络拦截器(Network Interceptor)之前,在每次 HTTP 请求/响应时被调用。通过拦截 Request 和 Response 对象,我们可以动态决定是否读取缓存、是否写入缓存、缓存有效期以及缓存键的生成方式,从而实现“请求级别”的缓存策略控制,而非全局固定配置。
4.2 OkHttp Interceptor 接口与调用时机
Interceptor 是一个单一方法的接口:
public interface Interceptor {
Response intercept(Chain chain) throws IOException;
}
Chain 对象提供 request()、proceed(Request)、connection() 和 call() 等方法。缓存拦截器通常注册为应用拦截器(通过 addInterceptor())以获得完整的控制权;若需要控制网络层缓存(如 Etag / Last-Modified),可注册为网络拦截器(addNetworkInterceptor())。
| 拦截器类型 | 注册方式 | 典型用途 |
|---|---|---|
| Application Interceptor | addInterceptor() | 全局请求头注入、缓存读写、重试、日志 |
| Network Interceptor | addNetworkInterceptor() | 重定向处理、压缩、网络层缓存条件请求 |
4.3 动态控制缓存策略的入口设计
为了让每个接口可以独立指定缓存行为,通常使用自定义注解(如 @CacheStrategy)或请求头标记。在 Retrofit 环境下,利用 @Header 或 @Tag 携带策略参数,拦截器解析后决定是否走本地缓存。常用的动态策略字段包括:
- cachePolicy:
NETWORK_ONLY、CACHE_ONLY、NETWORK_FIRST、CACHE_FIRST。 - cacheDuration:自定义缓存过期时长(秒)。
- cacheKey:自定义缓存键(默认使用 URL + query 参数)。
4.4 拦截器实现流程
- 提取策略标记:从 Request 的 header 或 tag 中获取缓存策略参数。
- 缓存决策:根据策略尝试从本地缓存(如磁盘/内存)中读取响应(需自行实现缓存存储)。
- 请求拦截:若策略允许网络并命中条件缓存,添加
If-None-Match或If-Modified-Since头部。 - 执行网络请求:调用
chain.proceed(modifiedRequest)。 - 响应拦截:检查状态码;若 304 未修改,使用缓存响应;否则将新响应写入缓存。
- 返回响应:构造最终 Response 返回给上一层。
下面是一个精简且完整的自定义缓存拦截器实现模板:
public class DynamicCacheInterceptor implements Interceptor {
private final CacheStorage cacheStorage; // 自定义缓存存储
public DynamicCacheInterceptor(CacheStorage cacheStorage) {
this.cacheStorage = cacheStorage;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
// 1. 解析动态缓存策略(通过自定义 header "X-Cache-Policy")
String policy = request.header("X-Cache-Policy");
if (policy == null) policy = "NETWORK_FIRST"; // 默认策略
String key = request.url().toString();
// 2. 尝试从缓存获取
if (policy.contains("CACHE")) {
CachedResponse cached = cacheStorage.get(key);
if (cached != null && !cached.isExpired()) {
if ("CACHE_ONLY".equals(policy) || "CACHE_FIRST".equals(policy)) {
// 直接返回缓存响应
return cached.toResponse(request);
}
// NETWORK_FIRST 或 CACHE_FIRST 时,携带条件缓存头
request = request.newBuilder()
.header("If-None-Match", cached.etag)
.header("If-Modified-Since", cached.lastModified)
.build();
}
}
// 3. 执行网络请求
Response response = chain.proceed(request);
// 4. 处理响应
if (response.code() == 304) {
// 服务端未修改,返回缓存数据
CachedResponse cached = cacheStorage.get(key);
if (cached != null) {
return cached.toResponse(request);
}
}
// 5. 更新缓存(仅当策略允许写入且响应可缓存)
if ("NETWORK_ONLY".equals(policy) || "NETWORK_FIRST".equals(policy) || "CACHE_FIRST".equals(policy)) {
if (response.isSuccessful()) {
CachedResponse cached = CachedResponse.fromResponse(response);
cacheStorage.put(key, cached);
}
}
return response;
}
}
4.5 动态策略注入方式(Retrofit 示例)
在接口方法上通过 @Header 传递策略:
public interface ApiService {
@GET("user/info")
Call<User> getUserInfo(
@Header("X-Cache-Policy") String cachePolicy
);
}
// 调用时动态指定
Call<User> call = apiService.getUserInfo("CACHE_FIRST");
@Tag 机制携带策略对象,避免污染请求头。在拦截器中通过 request.tag(CachePolicy.class) 获取。
4.6 内部流程图
下面展示了自定义缓存拦截器在 OkHttp 请求链中的核心逻辑和状态转换:
4.7 调试与验证
在开发阶段,启用 OkHttp 日志拦截器可观察缓存决策过程:
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.HEADERS);
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new DynamicCacheInterceptor(cacheStorage))
.addInterceptor(logging)
.build();
在 Logcat 中搜索 X-Cache-Policy 和 Cache-Control 关键字,确认策略是否生效。也可通过断点调试 intercept() 方法,检查 request 和 response 对象属性。
4.8 常见错误
- 缓存存储线程不安全:
CacheStorage必须支持并发读写,推荐使用ConcurrentHashMap或磁盘缓存加读写锁。 - 304 响应体为空:直接从缓存构造响应时需复制原始响应体,否则可能引发
closed异常。使用ResponseBody.create()封装。 - 缓存策略未传递:若使用
@Header方式,确保 Retrofit 调用时传入了正确参数;若使用 Tag,需在构建 Request 时设置 Tag。 - 网络请求被重试:当拦截器内部调用
chain.proceed()多次时,需注意 OkHttp 的重试机制可能导致重复写入缓存。
4.9 性能与稳定性优化
| 关注点 | 优化措施 |
|---|---|
| 缓存读写 I/O | 使用内存级缓存(如 LruCache)作为一级缓存,磁盘缓存作为二级,减少磁盘访问。 |
| 缓存键生成 | 避免使用完整 URL 作为键(可能过长),改用 MD5 或 SHA-1 缩短。 |
| 条件请求开销 | 仅在策略允许且缓存未过期时才添加条件头,避免无效的 304 网络往返。 |
| 内存泄漏 | 确保缓存存储不会持有 Activity/Context 引用;使用 WeakReference 或全局单例。 |
| 超时处理 | 在 intercept() 中设置自己的超时计时器,防止缓存读取阻塞请求线。 |
Cache 类。如果应用同时使用了 OkHttpClient.Builder.cache(),请确保两者策略一致,否则可能出现双重缓存或数据不一致问题。
4.10 部署集成
在 Application 初始化时构建 DynamicCacheInterceptor 实例,并将其添加到 Retrofit 的 OkHttpClient 构建器中。建议通过依赖注入(如 Dagger / Hilt)或单例模式管理 CacheStorage 的生命周期。
// Application 或 AppModule
CacheStorage cacheStorage = new DiskLruCacheStorage(getCacheDir(), 10 * 1024 * 1024);
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(new DynamicCacheInterceptor(cacheStorage))
.build();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(okHttpClient)
.build();
至此,自定义缓存拦截器已完整实现,可用于生产环境。下一阶段可结合 RxJava / Kotlin Flow 实现异步缓存读取,以进一步提升 UI 响应速度。