OkHttp 与 Retrofit:到底谁依赖谁?

很多人刚开始学 Retrofit 的时候,会以为它是个独立的网络库。其实——说白了,Retrofit 就是个「壳」,真正干活的是 OkHttp。我习惯把 Retrofit 比作一个智能调度员,而 OkHttp 是那个开着货车满街跑的司机。今天我们就来拆开看看,它们之间到底是怎么配合的。

OkHttp 拦截器链:请求的必经之路

OkHttp 最核心的设计就是 拦截器链(Interceptor Chain)。每个请求从发出到收到响应,会依次经过一系列拦截器。你可以把它想象成流水线上的工位,每个工位负责一件事。

// 一个典型拦截器链的顺序
1. 应用层拦截器(addInterceptor 添加)
2. RetryAndFollowUpInterceptor  (重试与重定向)
3. BridgeInterceptor            (桥接,处理请求头/响应头)
4. CacheInterceptor             (缓存)
5. ConnectInterceptor           (建立连接)
6. ... 网络层拦截器 ...
7. 应用层网络拦截器(addNetworkInterceptor 添加)

我自己当年刚接触时,总搞不清 addInterceptor 和 addNetworkInterceptor 有什么区别。其实很简单:前者只走一次,不管是否走缓存;后者只要真正发起网络请求就会走,包括重试。嗯,这里要注意:千万别用网络拦截器去打印日志,否则重试时会重复打印好几遍。我曾经就因为这个被老板骂过……

避坑指南:拦截器调用顺序是固定的,但你可以通过 addInterceptor(0, interceptor) 来插队到最前面。不过我不建议滥用,除非你真的清楚每次拦截器执行的时机。

连接池:TCP 连接的复用

HTTP/1.1 默认支持 keep-alive,但如果没有连接池,每次请求还是要经过三次握手。OkHttp 内置了 ConnectionPool,默认最多保持 5 个空闲连接,每个连接闲置 5 分钟后关闭。

参数默认值说明
maxIdleConnections5最多保留的空闲连接数
keepAliveDuration5 分钟空闲连接存活时间

你可能会问:「连接池大一点不是更好吗?」其实不是。我做过压测,连接池太大反而会占用过多文件描述符,移动端尤其明显。建议根据并发量调小一点,比如 2~3 个。

缓存机制:让网络少跑一趟

OkHttp 的缓存基于 HTTP 缓存协议(Cache-Control、ETag、Last-Modified 等)。它不像前端缓存那样自动缓存所有请求,你得提前告诉它「哪些路径可以缓存」。常用的做法是配合服务端返回的缓存头来做——如果服务器没返回缓存控制头,OkHttp 默认不缓存。

// 配置 10MB 磁盘缓存
File cacheDir = new File(context.getCacheDir(), "http");
Cache cache = new Cache(cacheDir, 10 * 1024 * 1024);

OkHttpClient client = new OkHttpClient.Builder()
    .cache(cache)
    .build();

这里有个容易翻车的点:POST 请求默认不缓存。虽然可以通过自定义拦截器强行缓存,但我不建议这么干——和 REST 规范冲突,容易出诡异的 bug。

警告:不要指望缓存能解决所有网络问题。我曾经试图用缓存来做离线模式,结果发现服务端返回的 Cache-Control 是 no-cache……最后还是老老实实走了本地数据库缓存。

Retrofit 如何基于 OkHttp 工作?

Retrofit 的核心流程是:接口定义 → 动态代理 → CallAdapter 适配 → OkHttp 执行。说白了,你写一个 interface ApiService,Retrofit 在运行时会生成一个代理对象。当你调用 getUser() 时,代理对象会把它翻译成一个 Request,交给 OkHttp 去发。

关键点在于:Retrofit 自己根本不解析 URL、不组装请求体,它只是把接口注解(@GET、@POST 等)转换成 OkHttp 的 Request.Builder。然后通过 OkHttp 的 Call 对象执行请求。你可以理解为:Retrofit 是「剧本」,OkHttp 是「演员」。

另外,Retrofit 的 Converter(Gson、Moshi 等)和 CallAdapter(RxJava、Kotlin Coroutines 等)也是挂载在 OkHttp 之上的。以 GsonConverterFactory 为例,它会在 onResponse 回调里拿到 ResponseBody 的输入流,然后用 Gson 解析成你的数据类。

小技巧:如果你想自定义异常处理(比如统一处理 401),不要在 Retrofit 的 Callback 里写死,而是加一个 OkHttp 的拦截器。这样一套逻辑所有接口共享,代码干净很多。

一张图看懂全局

OkHttp 拦截器链 · 连接池 · 缓存 · Retrofit 工作流程 Retrofit 层 动态代理 → CallAdapter Converter(Gson等) 产生 OkHttp Request Request OkHttp 拦截器链 应用拦截器 1..N RetryAndFollowUp BridgeInterceptor CacheInterceptor ConnectInterceptor 网络拦截器 1..N 连接池 复用 TCP 连接 maxIdleConnections=5 keepAliveDuration=5min 缓存机制 Cache-Control 解析 磁盘缓存 (Cache) ETag / Last-Modified 核心结论 Retrofit 生成 OkHttp 请求 → 进入拦截器链执行 → 连接池复用 TCP → 缓存控制 它们像三层蛋糕:顶层(Retrofit)定义行为,中间(拦截器链)处理流程,底层(连接池/缓存)优化性能

总结几个我个人常犯的错误

  • 拦截器顺序依赖:如果你在应用拦截器里修改了请求头,而连网拦截器又依赖那个头,那就要小心了——网络拦截器拿到的已经是修改后的版本。
  • 连接池在多域名场景下失效:连接池是按 host:port 分的,如果你的服务用了多个 CDN 域名,连接池效果会大打折扣。我曾在某次上线后才发现这个问题,赶紧让服务端把所有 API 收敛到一个域名下。
  • 缓存与动态 URL 冲突:如果你用 @Url 动态拼接参数,服务端返回的 Cache-Control 是 no-cache,那么缓存策略跟你预期完全不同——别问我怎么知道的。

理解了 OkHttp 这层底子,后面讲 Mock 拦截器你就知道该往哪插了。搞定了拦截器链,基本就掌握了 OkHttp 的「魂」。