引言
在 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
代替;volley
Google 2013年开源,基于
HttpURLConnection
的封装,具备良好的扩展性和适用性,不过对于简单申请或者大量网络申请时,性能较差。目前仍然有不少我的项目应用(通常是老代码的保护);okhttp
Square 2013年开源,基于 原生Http 的底层设计,具备 疾速 、 稳固 、节俭资源 等特点。是目前诸多热门网络申请库的底层实现,比方
Retrofit
、RxHttp
等;Retrofit
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.gradleimplementation "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
,即最后响应后果,而后将其返回上一个拦截器,即咱们的网络拦截器,再接着又向上返回,最终返回到咱们的一般拦截器处,从而实现整个链路的路由。
参照下面的流程,即大抵思路图如下:
<img src="https://cdn.staticaly.com/gh/Petterpx/ImageRespoisty@main/img/petterp-image.3v05jqplxmu0.png" alt="petterp-image" style="zoom:50%;" />
拦截器
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 篇,如果你感觉这个系列写的还不错,无妨点个关注催更一波,当然也能够看看其余篇:
- 由浅入深,详解 Lifecycle 的那些事
- 由浅入深,详解 LiveData 的那些事
- 由浅入深,详解 ViewModel 的那些事
- 由浅入深,详解 LeakCanary 的那些事
参阅
- 深刻了解OkHttp源码及设计思维
- OkHttp源码走心解析(很细 很长)
- 拆轮子系列:拆 OkHttp
- OkHttp 源码剖析
对于我
我是 Petterp ,一个 Android工程师 ,如果本文对你有所帮忙,欢送 点赞、评论、珍藏,你的反对是我继续创作的最大激励!