第二讲:Retrofit 与 OkHttp 的关系——拦截器、封装与缓存入口
这一讲我们聊聊 Retrofit 和 OkHttp 到底是怎么勾搭上的。很多人觉得 Retrofit 就是注解拼接口,但它跑起来的时候,真正的“打工人”其实是 OkHttp。我当年刚接触 Retrofit 时也困惑过:我只是调了个 api.getUser(),怎么底层就走了一整套网络栈?
说白了,Retrofit 是皮,OkHttp 是骨。Retrofit 负责把接口方法调用转换成 OkHttp 的 Request,然后丢给 OkHttp 去执行。那缓存呢?拦截器呢?全在 OkHttp 这层。
1. OkHttp 拦截器机制
OkHttp 最核心的设计就是 拦截器(Interceptor)。你可以把它想象成水管的滤芯——每个请求从你代码里发出,要经过一层层拦截器,最后才到达服务器;返回时再原路返回。我经常跟团队讲:拦截器是 OkHttp 的灵魂,也是我们优化缓存的唯一战场。
拦截器分两类:
- 应用拦截器(Application Interceptor)——加在
addInterceptor()里,在重试/重定向之前执行,能看到最原始的请求和最终响应。适合打日志、加公共 header。 - 网络拦截器(Network Interceptor)——加在
addNetworkInterceptor()里,在重试/重定向之后执行,能看到真正的网络往返。适合做缓存策略、改写Cache-Control。
避坑指南: 我曾经有个项目,在应用拦截器里写了缓存逻辑,结果发现重定向后缓存全部乱了。因为应用拦截器看不到重定向后的真正响应 URL。后来改成网络拦截器,问题才解决。记住一句话:缓存相关的拦截器,优先用网络拦截器。
拦截器链的执行顺序是这样的:
- 应用拦截器 1 → 应用拦截器 2 → …
- OkHttp 内部处理(重试、重定向、连接池)
- 网络拦截器 1 → 网络拦截器 2 → … → 真正的网络请求
- 响应反过来,从网络拦截器一路上到应用拦截器
代码上,一个简单的日志拦截器长这样:
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() 时,代理对象会:
- 解析方法上的注解(如
@GET、@Path),拼出okhttp3.Request - 把请求传给 Retrofit 内部的
OkHttpCall对象 OkHttpCall执行client.newCall(request).execute()(同步)或enqueue()(异步)- 拿到
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 中。