第二讲:Retrofit 与 OkHttp 的关系——拦截器、封装与缓存入口

这一讲我们聊聊 Retrofit 和 OkHttp 到底是怎么勾搭上的。很多人觉得 Retrofit 就是注解拼接口,但它跑起来的时候,真正的“打工人”其实是 OkHttp。我当年刚接触 Retrofit 时也困惑过:我只是调了个 api.getUser(),怎么底层就走了一整套网络栈?

说白了,Retrofit 是皮,OkHttp 是骨。Retrofit 负责把接口方法调用转换成 OkHttp 的 Request,然后丢给 OkHttp 去执行。那缓存呢?拦截器呢?全在 OkHttp 这层。

Retrofit 与 OkHttp 的关系与缓存配置入口 Retrofit 接口定义 Retrofit.Builder() OkHttpClient(含缓存配置) 拦截器链(拦截器列表) 应用拦截器 缓存拦截器 网络拦截器 网络请求 / 缓存命中 缓存配置入口 OkHttpClient.Builder().cache()

1. OkHttp 拦截器机制

OkHttp 最核心的设计就是 拦截器(Interceptor)。你可以把它想象成水管的滤芯——每个请求从你代码里发出,要经过一层层拦截器,最后才到达服务器;返回时再原路返回。我经常跟团队讲:拦截器是 OkHttp 的灵魂,也是我们优化缓存的唯一战场。

拦截器分两类:

  • 应用拦截器(Application Interceptor)——加在 addInterceptor() 里,在重试/重定向之前执行,能看到最原始的请求和最终响应。适合打日志、加公共 header。
  • 网络拦截器(Network Interceptor)——加在 addNetworkInterceptor() 里,在重试/重定向之后执行,能看到真正的网络往返。适合做缓存策略、改写 Cache-Control。

避坑指南: 我曾经有个项目,在应用拦截器里写了缓存逻辑,结果发现重定向后缓存全部乱了。因为应用拦截器看不到重定向后的真正响应 URL。后来改成网络拦截器,问题才解决。记住一句话:缓存相关的拦截器,优先用网络拦截器。

拦截器链的执行顺序是这样的:

  1. 应用拦截器 1 → 应用拦截器 2 → …
  2. OkHttp 内部处理(重试、重定向、连接池)
  3. 网络拦截器 1 → 网络拦截器 2 → … → 真正的网络请求
  4. 响应反过来,从网络拦截器一路上到应用拦截器

代码上,一个简单的日志拦截器长这样:

class LoggingInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        println("请求 URL: ${request.url} 方法: ${request.method}")
        val response = chain.proceed(request)
        println("响应 code: ${response.code}")
        return response
    }
}

// 使用
val client = OkHttpClient.Builder()
    .addInterceptor(LoggingInterceptor())
    .build()

2. Retrofit 如何封装 OkHttp

Retrofit 并没有自己造网络轮子。它的 create() 方法生成一个动态代理对象,当你调用 api.getUser() 时,代理对象会:

  1. 解析方法上的注解(如 @GET、@Path),拼出 okhttp3.Request
  2. 把请求传给 Retrofit 内部的 OkHttpCall 对象
  3. OkHttpCall 执行 client.newCall(request).execute()(同步)或 enqueue()(异步)
  4. 拿到 okhttp3.Response 后,通过你指定的 Converter(比如 Gson)转换成 Java/Kotlin 对象

你想想看,Retrofit 只负责“翻译”,真正的网络干活都是 OkHttp 在干。那缓存配置在哪配?

就在 OkHttpClient 上。 你构建 Retrofit 时传进去的 OkHttpClient,就是缓存配置的唯一入口。

个人习惯: 我一般会在项目里写一个 HttpClientProvider 类,统一创建单例的 OkHttpClient,在里面配好缓存、超时、拦截器。然后 Retrofit 直接引用这份 client。这样所有接口共享同一份缓存策略,避免遗漏。

// 统一提供 OkHttpClient
object HttpClientProvider {
    val client: OkHttpClient by lazy {
        OkHttpClient.Builder()
            .cache(Cache(cacheDir, cacheSize)) // 缓存配置
            .connectTimeout(30, TimeUnit.SECONDS)
            .addInterceptor(HeaderInterceptor())
            .addNetworkInterceptor(CacheInterceptor())
            .build()
    }
}

// Retrofit 使用
val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .client(HttpClientProvider.client)  // 关键:传入带缓存的 client
    .addConverterFactory(GsonConverterFactory.create())
    .build()

3. 缓存配置入口

缓存配置其实就两个地方,都跟 OkHttp 相关:

配置项 位置 说明
Cache 对象 OkHttpClient.Builder.cache(cache) 定义缓存目录、大小(如 10MB)。不设置默认无磁盘缓存。
Cache-Control 头 请求头 / 响应头 / 拦截器修改 控制缓存有效期、是否强制刷新。Retrofit 注解里加 @Headers 或在拦截器里改写。
@Headers 注解 Retrofit 接口方法 示例:@Headers("Cache-Control: max-age=60")
网络拦截器 addNetworkInterceptor() 统一修改所有请求/响应的 Cache-Control,实现全局缓存策略。

通常我们会在 网络拦截器 里动态调整缓存策略。比如,有网络时用 max-age=0 强制校验新鲜度,离线时用 max-stale 读取旧数据。这样用户没网时还能看到之前的数据,体验提升很明显。

class DynamicCacheInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val originalRequest = chain.request()
        
        // 无网络时使用缓存
        val request = if (!isNetworkAvailable()) {
            originalRequest.newBuilder()
                .cacheControl(CacheControl.FORCE_CACHE)
                .build()
        } else {
            // 有网络时,允许存储但必须校验新鲜度
            originalRequest.newBuilder()
                .header("Cache-Control", "max-age=0, only-if-cached")
                .build()
        }
        
        val originalResponse = chain.proceed(request)
        
        // 对响应也添加缓存控制
        return originalResponse.newBuilder()
            .header("Cache-Control", "public, max-age=60")
            .build()
    }
}

我曾经踩过的坑: 一开始只加了 Cache 对象,没配 Cache-Control,结果发现缓存根本不生效。因为 OkHttp 默认响应头里如果没有 Cache-Control,它就不会缓存。后来加上网络拦截器强制写入 Cache-Control: public, max-age=60,缓存才跑起来。说白了,光有 Cache 对象没用,还得告诉 OkHttp“这响应可以存多久”。

总结一下配置路径:

  • Step 1:创建 Cache 对象,指定目录和大小。
  • Step 2:在 OkHttpClient 构建时调用 .cache(cache)。
  • Step 3:通过拦截器(推荐网络拦截器)修改 Cache-Control 头,控制缓存规则。
  • Step 4:把这个 OkHttpClient 注入到 Retrofit 中。