🎯 OkHttp 的设计目标:简单、高效、可配置
OkHttp 的设计者希望提供一个既简单易用又足够强大的 HTTP 客户端。它的核心优势在于:
- 连接复用:通过 HTTP/2 和连接池,复用 TCP 连接,减少延迟。
- 内置缓存:遵守 HTTP 缓存语义,自动处理条件 GET 等,减少重复请求。
- 拦截器链:提供灵活的责任链模式,可以轻松添加日志、重试、请求头修改等功能。
- 无缝重试:当连接失败时,自动重试(对备用 IP 或路由)。
- 支持现代协议:HTTP/2、WebSocket 等。
🏗️ 整体架构:一个请求的生命周期
OkHttp 的核心设计可以用一个“拦截器链”来概括。当你发起一个请求时,它会经过一系列拦截器,每个拦截器负责一部分处理工作,最终将请求发送到服务器,并将响应返回。
下面是一个典型的 OkHttp 请求处理流程:
这个拦截器链是 OkHttp 最精髓的设计,它将请求处理的各个阶段解耦,同时又允许开发者无缝插入自定义逻辑。
🧩 核心组件详解
1. OkHttpClient
它是 OkHttp 的配置中心,通过建造者模式创建,一旦创建就是不可变的。主要配置项包括:
- 连接池:
ConnectionPool管理 HTTP/1.1 和 HTTP/2 的连接复用。 - 分发器:
Dispatcher控制并发请求数,维护同步/异步队列。 - 拦截器:
interceptors()和networkInterceptors()用于添加自定义拦截器。 - 缓存:
Cache指定缓存目录和大小。 - 超时配置:连接超时、读写超时、WebSocket 超时等。
- 代理、DNS、证书锁定等高级配置。
2. Request 和 Call
- Request:不可变对象,描述一次 HTTP 请求的所有信息:URL、方法、头、请求体。
- Call:表示一个准备执行的请求。它类似于一个任务,可以被同步执行(
execute())或异步执行(enqueue())。Call只能执行一次,之后可以调用clone()重新创建一个新的执行任务。 - RealCall 是
Call的实际实现类,内部持有OkHttpClient和Request。
3. Dispatcher(分发器)
Dispatcher 负责调度异步请求的执行,它内部维护了三个队列:
- 准备执行的异步请求队列:
readyAsyncCalls - 正在执行的异步请求队列:
runningAsyncCalls - 正在执行的同步请求队列:
runningSyncCalls
它通过 maxRequests(最大并发请求数,默认 64)和 maxRequestsPerHost(同一主机最大并发,默认 5)来控制并发。当异步请求被 enqueue 时,如果当前并发未达到限制,就会立即交给线程池执行;否则进入准备队列等待。
OkHttp 使用一个无界的 ThreadPoolExecutor 作为异步线程池,核心线程数为 0,最大线程数为 Integer.MAX_VALUE,线程空闲存活时间 60 秒。这意味着它可以根据需要创建大量临时线程,但长时间空闲后会自动回收。
4. Interceptor(拦截器)
拦截器是 OkHttp 的灵魂,它基于责任链模式。每个拦截器可以处理请求,并选择是否继续传递。OkHttp 内置了五个核心拦截器(按执行顺序):
a. 应用拦截器(通过 OkHttpClient.Builder.addInterceptor() 添加)
- 执行位置:在最开始,重试和重定向之前。
- 特点:无论发生多少次重定向或重试,只会执行一次。适合添加通用请求头、日志打印等。
b. RetryAndFollowUpInterceptor
- 职责:处理连接失败的重试(基于
retryOnConnectionFailure配置),以及 HTTP 重定向(如 3xx 状态码)。 - 实现:它会循环执行后续拦截器,如果出现
RouteException或IOException且可以重试,就重新发起请求;如果服务器返回重定向状态码,则构造新的请求继续。
c. BridgeInterceptor
- 职责:将用户构建的
Request转换为真正能发送的请求(补充必要的头部,如Content-Length、Transfer-Encoding、Host、User-Agent、Accept-Encoding等),并将网络返回的原始响应转换为用户友好的Response(例如自动解压 Gzip 响应体)。 - 特点:它桥接了应用层和网络层,处理了 cookie、gzip 等细节。
d. CacheInterceptor
- 职责:根据 HTTP 缓存协议,决定是否使用缓存响应,以及是否将网络响应缓存起来。
- 流程:
- 根据请求尝试从缓存中获取候选响应。
- 如果缓存可用且未过期,直接返回缓存响应。
- 否则,将请求传递给下一个拦截器(网络请求)。
- 得到网络响应后,根据缓存策略(如
Cache-Control)判断是否缓存,并将网络响应返回。
- 缓存存储:使用
DiskLruCache实现,缓存的 key 是请求的 URL 和其他缓存键的 md5 值。
e. ConnectInterceptor
- 职责:建立与目标服务器的连接。它从连接池中获取一个可用的
HttpCodec(用于编码/解码 HTTP 请求和响应的接口,实现类有Http1Codec和Http2Codec),并准备好输入输出流。 - 连接池:
ConnectionPool负责管理RealConnection(代表一个 socket 连接)。它通过一个清理线程定期扫描空闲连接并关闭。对于 HTTP/2,一个连接可以多路复用多个流。
f. 网络拦截器(通过 OkHttpClient.Builder.addNetworkInterceptor() 添加)
- 执行位置:在
ConnectInterceptor之后,CallServerInterceptor之前。 - 特点:能看到真正的网络请求和原始响应(包括重定向后的请求)。适合统计网络流量、监控等。
g. CallServerInterceptor
- 职责:链的末端,真正向服务器写入请求数据,并读取响应数据。
- 实现:利用
HttpCodec发送请求头、请求体(如果有),然后读取响应头、响应体,构建最终的Response。
🔗 连接池与连接复用
OkHttp 通过 ConnectionPool 复用 TCP 连接,减少握手开销。
- 连接存储:
ConnectionPool内部维护一个Deque<RealConnection>和清理线程。 - 连接复用策略:
- 对于 HTTP/1.1:一个连接只能处理一个请求/响应(串行),但可以通过
keep-alive复用连接处理后续请求。 - 对于 HTTP/2:一个连接可以同时承载多个并发的流(请求/响应),实现多路复用。
- 对于 HTTP/1.1:一个连接只能处理一个请求/响应(串行),但可以通过
- 连接选择:当需要连接一个地址时,
ExchangeFinder会尝试从连接池中获取一个匹配的连接(相同主机、端口、协议等)。如果获取不到,才创建新连接。 - 空闲连接清理:清理线程每隔一段时间扫描连接池,关闭超过
keepAliveDuration且未被使用的空闲连接。
📦 缓存机制
OkHttp 的缓存完全遵循 HTTP 缓存规范(RFC 7234)。开发者通过设置 Cache 目录和大小来启用缓存。
- 缓存存储:使用
DiskLruCache将响应元数据(头部)和响应体分开存储。 - 缓存策略:
CacheStrategy类根据请求和缓存的响应,决定是使用缓存、发起网络请求,还是两者结合(如条件 GET)。 - 条件 GET:当缓存过期但可能仍有价值时,
CacheInterceptor会在请求中添加If-None-Match或If-Modified-Since,如果服务器返回 304,则直接使用缓存的响应。
🔄 重试与重定向
RetryAndFollowUpInterceptor 负责这两个功能:
- 重试:当请求因连接异常(如路由不可达)失败,且
retryOnConnectionFailure为 true 时,它会尝试使用下一个备选路由(如果有)重新请求。默认最多重试一次(但可通过自定义拦截器修改)。 - 重定向:当服务器返回 3xx 状态码时,它会构造一个新的请求指向新地址,并重新执行拦截器链。默认最多跟踪 20 次重定向。
📡 WebSocket 支持
OkHttp 提供了对 WebSocket 的原生支持,通过 WebSocketListener 和 WebSocket 接口。它复用了相同的连接池和拦截器机制,但在 CallServerInterceptor 之后,会升级协议到 WebSocket。
🔧 与 Retrofit 的关系
Retrofit 在底层默认使用 OkHttp 作为其网络层。当你通过 Retrofit 的 CallAdapter 和 Converter 构建出 Call 对象时,最终会委托给 OkHttp 的 Call 执行。两者配合得天衣无缝:Retrofit 负责声明和转换,OkHttp 负责传输和连接管理。
💡 总结
OkHttp 之所以如此强大,源于其精心的架构设计:
- 责任链模式(拦截器) 将请求处理分解为可插拔的阶段,兼顾了可扩展性与可维护性。
- 连接池与多路复用 极大提升了网络性能。
- 内置的缓存、重试、重定向 功能,让开发者无需重复造轮子。
- 线程池与分发器 合理管理并发,避免资源耗尽。
理解 OkHttp 的工作原理,不仅能帮助我们更好地使用它,还能在遇到网络问题时快速定位原因,甚至写出更优雅的自定义拦截器。