由浅入深,聊聊OkHttp的那些事(很长,很细节)
引言
在 Android
开发的世界中,有一些组件,无论应用层技术再怎么迭代,作为基础支持,它们依然在那里。
比如当我们提到网络库时,总会下意识想到一个名字,即 OkHttp
。
尽管对于大多数开发者而言,通常情况下使用的是往往它的封装版本 Retrofit
,不过其底层依然离不开 Okhttp
作为基础支撑。而无论是自研网络库的二次封装,还是个人使用,OkHttp
也往往都是不二之选。
故本篇将以最新视角开始,用力一瞥 OkHttp
的设计魅力。
本文对应的 OkHttp
版本: 4.10.0
本篇定位 中高难度,将从背景到使用方式,再到设计思想与源码解析,尽可能全面、易懂。
背景
每一个技术都有其变迁的历史背景与特性,本小节,我们将聊一聊 Android网络库 的迭代史,作为开篇引语,润润眼。 🔖
关于 Android网络库 的迭代历史,如下图所示:
具体进展如下:
HttpClient
Android1.0
时推出。但存在诸多问题,比如内存泄漏,频繁的GC等。5.0后,已被弃用;HttpURLConnection
Android2.2
时推出,比HttpClient
更快更稳定,Android4.4 之后底层已经被Okhttp
替代;Google 2013年开源,基于
HttpURLConnection
的封装,具有良好的扩展性和适用性,不过对于复杂请求或者大量网络请求时,性能较差。目前依然有不少项目使用(通常是老代码的维护);Square 2013年开源,基于 原生Http 的底层设计,具有 快速 、 稳定 、节省资源 等特点。是目前诸多热门网络请求库的底层实现,比如
Retrofit
、RxHttp
等;Square 2013年开源,基于
OkHttp
的封装,目前 主流 的网络请求库。通过注解方式配置网络请求、REST风格 api、解耦彻底、经常会搭配 Rx等 实现 框架联动;
…
上述的整个过程,也正是伴随了 Android
开发的各个时期,如果将上述分为 5个阶段 的话,那么则为:
HttpClient
->HttpURLConnection
->volley
->okhttp
->Retrofit
*
通过 Android网络库 的迭代历史,我们不难发现,技术变迁越来越趋于稳定,而 OkHttp
也已经成为了基础组件中不可所缺的一员。
设计思想
当聊到OkHttp的设计思想,我们想知道什么?
从应用层去看,熟练的开发者会直接喊出拦截器,巴拉巴拉…
而作为初学者,可能更希望的事广度与解惑,
OkHttp
到底牛在了什么地方,或者说常说的 拦截器到底是什么 ? 🧐
在官方的描述中,OkHttp
是一个高效的 Http请求框架 ,旨在 简化 客户端网络请求,提高 请求效率。
具体设计思想与特性如下:
- 连接复用 :避免在每个请求之间重新建立连接。
- 连接池 降低了请求延迟 (HTTP/2不可用情况下);
- 自动重试 :在请求失败时自动重试请求,从而提高请求可靠性。
- 自动处理缓存 :会按照预定的缓存策略处理缓存,以便最大化网络效率。
- 支持HTTP/2, 并且允许对同一个主机的所有请求共享一个套接字(HTTP/2);
- 简化Api:Api设计简单明了,易于使用,可以轻松发起请求获取响应,并处理异常。
- 支持gzip压缩 :OkHttp支持gzip压缩,以便通过减少网络数据的大小来提高网络效率。
特别的,如果我们的服务器或者域名有 多个IP地址 ,OkHttp
将在 第一次 连接失败时尝试替代原有的地址(对于 IPv4+IPv6 和托管在冗余数据中心的服务是必需的)。并且支持现代 TLS 功能(TLS 1.3、ALPN、证书固定)。它可以配置为回退以实现广泛的连接。
总的来说,其设计思想是通过 简化请求过程 、提高请求效率、提高请求可靠性,从而提供 更快的响应速度 。
应用层的整个请求框架图如下:
使用方式
在开始探究设计原理与思想之前,我们还是要先看看最基础的使用方式,以便为后续做一些铺垫。
// build.gradle
implementation "com.squareup.okhttp3:okhttp:4.10.0"
复制代码
// Android Manifest
<uses-permission android:name="android.permission.INTERNET" />
复制代码
发起一个get请求
拦截器的使用
总结起来就是下面几步:
- 创建
OkHttpClient
对象;- 构建
Request
;- 调用
OkHttpClient
执行request
请求 ;- 同步阻塞 或者 异步回调 方式接收结果;
更多使用方式,可以在搜索其他同学的教程,这里仅仅只是作为后续解析原理时的必要基础支撑。
源码分析
基础配置
OkHttpClient
val client = OkHttpClient.Builder().xxx.build()
复制代码
由上述调用方式,我们便可以猜出,这里使用了 构建者模式 去配置默认的参数,所以直接去看 OkHttpClient.Builder
支持的参数即可,具体如下:
具体的属性意思在代码中也都有注释,这里我们就不在多提了。
需要注意的是,在使用过程中,对于 OkHttpClient
我们还是应该缓存下来或者使用单例模式以便后续复用,因为其相对而言还是比较重。
Request
指客户端发送到服务器的 HTTP请求。
在 OkHttp
中,可以使用 Request
对象来构建请求,然后使用 OkHttpClient
对象来发送请求。
通常情况下,一个请求包括了 请求头、请求方法、请求路径、请求参数、url地址 等信息。主要是用来请求服务器返回某些资源,如网页、图片、数据等。
具体源码如下所示:
Request.Builder().url("https://www.baidu.com").build()
复制代码
open class Builder {
// url地址
internal var url: HttpUrl? = null
// 请求方式
internal var method: String
// 请求头
internal var headers: Headers.Builder
// 请求体
internal var body: RequestBody? = null
// 请求tag
internal var tags: MutableMap<Class<*>, Any>
}
复制代码
发起请求
execute()
用于执行 同步请求 时调用,具体源码如下:
client.newCall(request).execute()
复制代码
接下来我们再去看看 client.newCall()
, 即请求发起时的逻辑。
当我们使用 OkHttpClient.newCall()
方法时,实际是创建了一个新的 RealCall
对象,用于 应用层与网络层之间的桥梁,用于处理连接、请求、响应以及流 ,其默认构造函数中需要传递 okhttpClient
对象以及 request
。
接着,使用了 RealCall
对象调用了其 execute()
方法开始发起请求,该方法内部会将当前的 call
加入我们 Dispatcher
分发器内部的 runningSyncCalls
队列中取,等待被执行。接着调用 getResponseWithInterceptorChain()
,使用拦截器获取本次请求响应的内容,这也即我们接下来要关注的步骤。
enqueue()
执行 异步请求 时调用,具体源码如下:
client.newCall(request).enqueue(CallBack)
复制代码
当我们调用 RealCall.enqueue()
执行异步请求时,会先将本次请求加入 Dispather.readyAsyncCalls
队列中等待执行,如果当前请求是 webSocket
请求,则查找与当前请求是同一个 host
的请求,如果存在一致的请求,则复用先前的请求。
接下来调用 promoteAndExecute()
将所有符合条件可以请求的 Call
从等待队列中添加到 可请求队列 中,再遍历该请求队列,将其添加到 线程池 中去执行。
继续沿着上面的源码,我们去看 asyncCall.executeOn(executorService)
,如下所示:
上述逻辑也很简单,当我们将任务添加到线程池后,当任务被执行时,即触发 run()
方法的调用。该方法中会去调用 getResponseWithInterceptorChain()
从而使用拦截器链获取服务器响应,从而完成本次请求。请求成功后则调用我们开始时的 callback对象 的 onResponse()
方法,异常(即失败时)则调用 onFailure()
方法。
拦截器链
在上面我们知道,他们最终都走到了 RealCall.getResponseWithInterceptorChain()
方法,即使用 拦截器链 获取本次请求的响应内容。不过对于初看OkHttp源码的同学,这一步应用会有点迷惑,拦截器链 是什么东东👾?
在解释 拦截器链 之前,我们不妨先看一下 RealCall.getResponseWithInterceptorChain()
方法对应的源码实现,然后再去解释为什么,也许更容易理解。
具体源码如下:
上述的逻辑非常简单,内部会先创建一个局部拦截器集合,然后将我们自己设置的普通拦截器添加到该集合中,然后添加核心的5大拦截器,接着再将我们自定义的网络拦截器也添加到该集合中,最终才添加了真正用于执行网络请求的拦截器。接着创建了一个拦截器责任链 RealInterceptorChain
,并调用其 proceed()
方法开始执行本次请求。
责任链模式
在上面我们说到了,要解释 OkHttp
的拦截器链,我们有必要简单聊一下什么是责任链模式?
责任链模式(Chain of Responsibility)是一种处理请求的模式,它让多个处理器都有机会处理该请求,直到其中某个处理成功为止。责任链模式把多个处理器串成链,然后让请求在链上传递。
摘自 责任链模式 @廖雪峰
以 Android
中常见的事件分发为例:当我们的手指点击屏幕开始,用户的触摸事件从 Activity
开始分发,接着从 windows
开始分发到具体的 contentView(ViewGroup)
上,开始调用其 dispatchTouEvent()
方法进行事件分发。在这个方法内,如果当前 ViewGroup
不进行拦截,则默认会继续向下分发,寻找当前 ViewGroup
下对应的触摸位置 View
,如果该 View
是一个 ViewGroup
,则重复上述步骤。如果事件被某个 view
拦截,则触发其 onTouchEvent()
方法,接着交由该view去消费该事件。而如果事件传递到最上层 view
还是没人消费,则该事件开始按照原路返回,先交给当前 view
自己的 onTouchEvent()
,因为自己不消费,则调用其 父ViewGroup
的 onTouchEvent()
,如此层层传递,最终又交给了 Act
自行处理。上述这个流程,就是 责任链模式 的一种体现。
如下图所示:
上图来自 Android事件分发机制三:事件分发工作流程 @一只修仙的猿
看完什么是责任链模式,让我们将思路转回到 OkHttp
上面,我们再去看一下 RealInterceptorChain
源码。
上述逻辑如下:
当
getResponseWithInterceptorChain()
方法内部最终调用RealInterceptorChain.proceed()
时,内部传入了一个默认的index ,这个 index 就代表了当前要调用的 拦截器item ,并在方法内部每次创建一个新的RealInterceptorChain
链,index+1,再调用当前拦截器intercept()
方法时,然后将下一个链传入;最开始调用的是用户自定义的 普通拦截器,如果上述我们添加了一个
CustomLogInterceptor
的拦截器,当获取response
时,我们需要调用Interceptor.Chain.proceed()
,而此时的chain
正是下一个拦截器对应的RealInterceptorChain
;上述流程里,index从0开始,以此类推,一直到链条末尾,即 拦截器集合长度-1处;
当遇到最后一个拦截器
CallServerInterceptor
时,此时因为已经是最后一个拦截器,链条肯定要结束了,所以其内部肯定也不会调用proceed()
方法。相应的,为什么我们在前面说 它 是真正执行与服务器建立实际通讯的拦截器?
因为这个里会获取与服务器通讯的
response
,即最初响应结果,然后将其返回上一个拦截器,即我们的网络拦截器,再接着又向上返回,最终返回到我们的普通拦截器处,从而完成整个链路的路由。
参照上面的流程,即大致思路图如下:
拦截器
RetryAndFollowUpInterceptor
见名知意,用于 请求失败 的 重试 工作以及 重定向 的后续请求工作,同时还会对 连接 做一些初始化工作。
上述的逻辑,我们分为四段进行分析:
- 请求时如果遇到异常,则根据情况去尝试恢复,如果不能恢复,则抛出异常,跳过本次请求;如果请求成功,则在
finally
里释放资源; - 如果请求是重试之后的请求,那么将重试前请求的响应体设置为null,并添加到当前响应体的
priorResponse
字段中; - 根据当前的responseCode判断是否需要重试,若不需要,则返回
response
;若需要,则返回request
,并在后续检查当前重试次数是否达到阈值; - 重复上述步骤,直到步骤三成功。
在第一步时,获取 response
时,需要调用 realChain.proceed(request)
,如果你还记得上述的责任链,所以这里触发了下面的拦截器执行,即 BridgeInterceptor
。
BridgeInterceptor
用于 客户端和服务器 之间的沟通 桥梁 ,负责将用户构建的请求转换为服务器需要的请求。比如添加 content-type
、cookie
等,再将服务器返回的 response
做一些处理,转换为客户端所需要的 response
,比如移除 Content-Encoding
,具体见下面源码所示:
上述逻辑如下:
- 首先调用
chain.request()
获取原始请求数据,然后开始重新构建请求头,添加header
以及cookie
等信息; - 将第一步构建好的新的
request
传入chain.proceed()
,从而触发下一个拦截器的执行,并得到 服务器返回的response
。然后保存response
携带的cookie
,并移除header
中的Content-Encoding
和Content-Length
,并同步修改body
。
CacheInterceptor
见名知意,其用于网络缓存,开发者可以通过 OkHttpClient.cache()
方法来配置缓存,在底层的实现处,缓存拦截器通过 CacheStrategy
来判断是使用网络还是缓存来构建 response
。具体的 cache
策略采用的是 DiskLruCache
。
Cache的策略如下图所示:
具体源码如下所示:
具体的逻辑如上图所示,具体可以参照上述的 Cache
流程图,这里我们再说一下 CacheStrategy
这个类,即决定何时使用 网络请求、响应缓存。
CacheStrategy
ConnectInterceptor
实现与服务器真正的连接。
上述流程如下:
- 初始化 一个
exchange
对象; - 根据
exchange
对象来复制创建一个新的连接责任链; - 执行该连接责任链。
那 Exchange 是什么呢?
在官方的解释里,其用于 传递单个
HTTP
请求和响应对,在ExchangeCode
的基础上担负了一些管理及事件分发的作用。具体而言,
Exchange
与Request
相对应,新建一个请求时就会创建一个Exchange
,该Exchange
负责将这个请求发送出去并读取到响应数据,而具体的发送与接收数据使用的则是ExchangeCodec
。
相应的,ExchangeCode 又是什么呢?
ExchangeCodec
负责对request
编码及解码Response
,即写入请求及读取响应,我们的请求及响应数据都是通过它来读写。通俗一点就是,ExchangeCodec 是请求处理器,它内部封装了
OkHttp
中执行网络请求的细节实现,其通过接受一个Request
对象,并在内部进行处理,最终生成一个符合HTTP
协议标准的网络请求,然后接受服务器返回的HTTP响应,并生成一个Response
对象,从而完成网络请求的整个过程。
额外的,我们还需要再提一个类,ExchangeFinder 。
用于寻找可用的
Exchange
,然后发送下一个请求并接受下一个响应。
虽然上述流程看起来似乎很简单,但我们还是要分析下具体的流程,源码如下所示:
RealCall.initExchange()
初始化 Exchage
的过程。
从 ExchangeFinder
找到一个新的或者已经存在的 ExchangeCodec
,然后初始化 Exchange
,以此来承载接下来的HTTP请求和响应对。
ExchangeFinder.find()
查找 ExchangeCodec
(请求响应编码器) 的过程。
接下来我们看看查找 RealConnection
的具体过程:
上述的整个流程如下:
上述会先通过 ExchangeFinder
去 RealConnecionPool
中尝试寻找已经存在的连接,未找到则会重新创建一个 RealConnection
(连接) 对象,并将其添加到连接池里,开始连接。然后根据找到或者新创建 RealConnection
对象,并根据当前请求协议创建不同的 ExchangeCodec
对象并返回,最后初始化一个 Exchange
交换器并返回,从而实现了 Exchange
的初始化过程。
在具体找寻 RealConnection
的过程中,一共尝试了5次,具体如下:
- 尝试重连
call
中的connection
,此时不需要重新获取连接; - 尝试从连接池中获取一个连接,不带路由与多路复用;
- 再次尝试从连接池中获取一个连接,带路由,不带多路复用;
- 手动创建一个新连接;
- 再次尝试从连接池中获取一个连接,带路由与多路复用;
当 Exchange
初始化完成后,再复制该对象创建一个新的 Exchange
,并执行下一个责任链,从而完成连接的建立。
networkInterceptors
网络拦截器,即 client.networkInterceptors 中自定义拦截器,与普通的拦截器 client.interceptors
不同的是:
由于网络拦截器处于倒数第二层,在 RetryAndFollowUpInterceptor
失败或者 CacheInterceptor
返回缓存的情况下,网络拦截器无法被执行。而普通拦截器由于第一步就被就执行到,所以不受这个限制。
CallServerInterceptor
链中的最后一个拦截器,也即与服务器进行通信的拦截器,利用 HttpCodec
进行数据请求、响应数据的读写。
具体源码如下:
先写入要发送的请求头,然后根据条件判断是否写入要发送的请求体。当请求结束后,解析服务器返回的响应头,构建一个新的 response
并返回;如果 response.code
为 100,则重新读取响应体并构建新的 response
。因为这是最底层的拦截器,所以这里肯定不会再调用 proceed()
再往下执行。
小结
至此,关于 OkHttp
的分析,到这里就结束了。为了便于理解,我们再串一遍整个思路:
在 OkHttp
中,RealCall
是 Call
的实现类,其负责 执行网络请求 。其中,请求 request
由 Dispatcher
进行调度,其中 异步调用 时,会将请求放到到线程池中去执行; 而同步的请求则只是会添加到 Dispatcher
中去管理,并不会有线程池参与协调执行。
在具体的请求过程中,网络请求依次会经过下列拦截器组成的责任链,最后发送到服务器。
- 普通拦截器,
client.interceptors()
; - 重试、重定向拦截器
RetryAndFollowUpInterceptor
; - 用于客户端与服务器桥梁,将用户请求转换为服务器请求,将服务器响应转换为用户响应的的
BridgeInterceptor
; - 决定是否需要请求服务器并写入缓存再返回还是直接返回服务器响应缓存的
CacheInterceptor
; - 与服务器建立连接的
ConnectInterceptor
; - 网络拦截器,
client.networkInterceptors()
; - 执行网络请求的
CallServerInterceptor
;
而相应的服务器响应体则会从 CallServerInterceptor
开始依次往前开始返回,最后由客户端进行处理。
需要注意的是,当我们
RetryAndFollowUpInterceptor
异常或者CacheInterceptor
拦截器直接返回了有效缓存,后续的拦截器将不会执行。
常见问题
OkHttp如何判断缓存有效性?
这里其实主要说的是 CacheInterceptor
拦截器里的逻辑,具体如下:
OkHttp
使用 HTTP协议 中的 缓存控制机制 来判断缓存是否有效。如果请求头中包含 "Cache-Control"
和 "If-None-Match"
/ "If-Modified-Since"
字段,OkHttp
将根据这些字段的值来决定是否使用缓存或从网络请求响应。
Cache-Control
指 包含缓存控制的指令,例如 "no-cache" 和 "max-age" ;
If-None-Match
指 客户端缓存的响应的ETag值,如果服务器返回相同的 ETag 值,则说明响应未修改,缓存有效;
If-Modified-Since
指 客户端缓存的响应的最后修改时间,如果服务器确定响应在此时间后未更改,则返回304 Not Modified状态码,表示缓存有效。
相应的,OkHttp
也支持自定义缓存有效性控制,开发者可以创建一个 CacheControl
对象,并将其作为请求头添加到 Request
中,如下所示:
// 禁止OkHttp使用缓存
val cacheControl = CacheControl.Builder()
.noCache()
.build()
val request = Request.Builder()
.cacheControl(cacheControl)
.url("https://www.baidu.com")
.build()
复制代码
OkHttp如何复用TCP连接?
这个其实主要说的是 ConnectInterceptor
拦截器中初始化 Exchange
时内部做的事,具体如下:
OkHttp
使用连接池 RealConnectionPool
管理所有连接,连接池将所有活动的连接存储在池中,并维护了一个空闲的连接列表(TaskQueue
),当需要新的连接时,优先尝试从这个池中找,如果没找到,则 重新创建 一个 RealConnection
连接对象,并将其添加到连接池中。在具体的寻找连接的过程中,一共进行了下面5次尝试:
- 尝试重连
RealCall
中的connection
,此时不需要重新获取连接; - 尝试从连接池中获取一个连接,不带路由与多路复用;
- 再次尝试从连接池中获取一个连接,带路由,不带多路复用;
- 手动创建一个新连接;
- 再次尝试从连接池中获取一个连接,带路由与多路复用;
当然 OkHttp
也支持自定义连接池,具体如下:
上述代码中,创建了一个新的连接池,并设置其保留最多 maxIdleConnections
个空闲连接,并且连接的存活期为 keepAliveDuration
分钟。
OKHttp复用TCP连接的好处是什么?
OkHttp
是由连接池管理所有连接,通过连接池,从而可以限制连接的 最大数量,并且对于空闲的连接有相应的 存活期限 ,以便在长时间不使用后关闭连接。当请求结束时,并且将保留该连接,便于后续 复用 。从而实现了在多个请求之间共享连接,避免多次建立和关闭TCP连接的开销,提高请求效率。
OkHttp中的请求和响应 与 网络请求和响应,这两者有什么不同?
OkHttp
中的的请求和响应指的是客户端创建的请求对象 Request
和 服务端返回的响应对象 Response
,这两个对象用于定义请求和响应的信息。网络请求和响应指的是客户端向服务端发送请求,服务端返回相应的过程。
总的来说就是,请求和响应是应用程序内部自己的事,网络请求和响应则是发生在网络上的请求和响应过程。
OkHttp 应用拦截器和网络拦截器的区别?
- 从调用方式上而言,应用拦截器指的是
OkhttpClient.intercetors
,网络拦截器指的是OkHttpClient.netIntercetors
。 - 从整个责任链的调用来看,应用拦截器一定会被执行一次,而网络拦截器不一定会执行或者执行多次情况,比如当我们
RetryAndFollowUpInterceptor
异常或者CacheInterceptor
拦截器直接返回了有效缓存,后续的拦截器将不会执行,相应的网络拦截器也自然不会执行到;当我们发生 错误重试 或者 网络重定向 时,网络拦截器此时可能就会执行多次。 - 其次,除了
CallServerInterceptor
与CacheIntercerceptor
缓存有效之外,每个拦截器都应该至少调用一次realChain.proceed()
方法。但应用拦截器可以调用多次processed()
方法,因为其在请求流程中是可以递归调用;而网络拦截器只能调用一次processed()
方法,否则将导致请求重复提交,影响性能,另外,网络拦截器没有对请求做修改的可能性,因此不需要再次调用processed()
方法。 - 从 使用方式的 本质而言,应用拦截器可以 拦截和修改请求和响应 ,但 不能修改网络请求和响应 。比如使用应用拦截器添加请求参数、缓存请求结果;网络拦截器可以拦截和修改网络请求和响应。例如使用网络拦截器添加请求头、修改请求内容、检查响应码等。
- 在相应的执行顺序上,网络拦截器是
先进先出(FIFO)
,应用拦截器是先进后出(FILO)
的方式执行。
结语
本篇中,我们从网络库的迭代历史,一直到 OkHttp
的使用方式、设计思想、源码探索,最后又聊了聊常见的一些问题,从而较系统的了解了 OkHttp
的方方面面,也解释了 OkHttp应用层
的相关问题,当然这些问题我相信也仅仅只是冰山一角🧩。 更多面试相关,或者实际问题,仍需要我们自己再进行完善,从而形成全面的透析力。
这篇文章断断续续写了将近两周,其中肯定有不少部分存在缺陷或者逻辑漏洞,如果您发现了,也可以告诉我。
通过这篇文章,于我个人而言,也是完成了对于 OkHttp应用层
一次较系统的了解,从而也完善了知识拼图中重要的一块,期待作为读者的你也能有如此或者更深的体会。🏃🏻
更多
这是 解码系列 - OkHttp 篇,如果你觉得这个系列写的还不错,不妨点个关注催更一波,当然也可以看看其他篇:
参阅
链接:https://juejin.cn/post/7199431845367922745
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。