4. 自定义缓存拦截器设计:实现 Interceptor 接口、请求/响应拦截、动态控制缓存策略入口

4.1 原理概述

OkHttp 的 Interceptor 接口允许开发者在请求链中插入自定义逻辑。缓存拦截器位于应用拦截器(Application Interceptor)之后、网络拦截器(Network Interceptor)之前,在每次 HTTP 请求/响应时被调用。通过拦截 Request 和 Response 对象,我们可以动态决定是否读取缓存、是否写入缓存、缓存有效期以及缓存键的生成方式,从而实现“请求级别”的缓存策略控制,而非全局固定配置。

核心思想: 利用 Interceptor 的 chain.proceed() 前/后两个时间点,在请求发出前检查缓存命中,在响应返回后更新缓存,配合自定义头部标记或注解驱动动态策略。

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 InterceptoraddInterceptor()全局请求头注入、缓存读写、重试、日志
Network InterceptoraddNetworkInterceptor()重定向处理、压缩、网络层缓存条件请求

4.3 动态控制缓存策略的入口设计

为了让每个接口可以独立指定缓存行为,通常使用自定义注解(如 @CacheStrategy)或请求头标记。在 Retrofit 环境下,利用 @Header 或 @Tag 携带策略参数,拦截器解析后决定是否走本地缓存。常用的动态策略字段包括:

  • cachePolicy:NETWORK_ONLY、CACHE_ONLY、NETWORK_FIRST、CACHE_FIRST。
  • cacheDuration:自定义缓存过期时长(秒)。
  • cacheKey:自定义缓存键(默认使用 URL + query 参数)。

4.4 拦截器实现流程

  1. 提取策略标记:从 Request 的 header 或 tag 中获取缓存策略参数。
  2. 缓存决策:根据策略尝试从本地缓存(如磁盘/内存)中读取响应(需自行实现缓存存储)。
  3. 请求拦截:若策略允许网络并命中条件缓存,添加 If-None-Match 或 If-Modified-Since 头部。
  4. 执行网络请求:调用 chain.proceed(modifiedRequest)。
  5. 响应拦截:检查状态码;若 304 未修改,使用缓存响应;否则将新响应写入缓存。
  6. 返回响应:构造最终 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");
进阶方案: 使用 OkHttp 的 @Tag 机制携带策略对象,避免污染请求头。在拦截器中通过 request.tag(CachePolicy.class) 获取。

4.6 内部流程图

下面展示了自定义缓存拦截器在 OkHttp 请求链中的核心逻辑和状态转换:

自定义缓存拦截器流程 客户端请求进入 提取 X-Cache-Policy 缓存可用? 返回缓存响应 添加条件请求头 chain.proceed() 是否 304? 更新缓存并返回 返回最终响应

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() 中设置自己的超时计时器,防止缓存读取阻塞请求线。
注意: 自定义缓存拦截器会绕过 OkHttp 内置的 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 响应速度。