携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第7天,点击查看活动详情
前言
OkHttp源码系列文章:
- OkHttp源码之深度解析(一)——整体框架分析
- OkHttp源码之深度解析(二)——拦截器链详解:责任链模式
- OkHttp源码之深度解析(三)——分发器Dispatcher详解
- OkHttp源码之深度解析(四)——RetryAndFollowUpInterceptor详解:重试机制
- OkHttp源码之深度解析(五)——CacheInterceptor详解:缓存机制
拦截器链是整个OkHttp框架的核心所在,用户发起的请求经过拦截器链的层层拦截获取Response,Response根据拦截器链原路返回最终到达用户手上,整个拦截器链的实现使用了责任链设计模式,其中的每个拦截器只需要关心自己的职责而不需要对上一个或下一个拦截器负责。拦截器链具体是怎么实现的,是如何工作的,本文将对此进行详细分析。
PS:本文基于OkHttp3版本4.9.3
拦截器
Interceptor接口
先来看看Interceptor接口的源码:
fun interface Interceptor {
@Throws(IOException::class)
fun intercept(chain: Chain): Response
companion object {
inline operator fun invoke(crossinline block: (chain: Chain) -> Response): Interceptor =
Interceptor { block(it) }
}
interface Chain {
fun request(): Request
@Throws(IOException::class)
fun proceed(request: Request): Response
fun connection(): Connection?
fun call(): Call
fun connectTimeoutMillis(): Int
fun withConnectTimeout(timeout: Int, unit: TimeUnit): Chain
fun readTimeoutMillis(): Int
fun withReadTimeout(timeout: Int, unit: TimeUnit): Chain
fun writeTimeoutMillis(): Int
fun withWriteTimeout(timeout: Int, unit: TimeUnit): Chain
}
}
复制代码
Interceptor接口是OkHttp内置的所有拦截器的父类,接口里面声明了一个拦截方法intercept,具体的拦截并获取Response的逻辑在子类中实现。Interceptor内部还定义了一个Chain接口,这是用于将所有拦截器连接起来的链条,Chain接口里的方法中真正用于处理请求获取Response的是proceed方法,OkHttp框架中的拦截器链RealInterceptorChain就是实现了这个Chain接口,至于具体是怎么实现的先不管,后文会进行具体分析,这里有个大概印象即可。
各类拦截器
接下来就来看看拦截器链有哪些拦截器,在本系列的第一篇文章中已经介绍过拦截器链是在getResponseWithInterceptorChain方法中构建的,在构建拦截器链之前会先将所有拦截器按顺序加入到同一个列表中:
val interceptors = mutableListOf<Interceptor>()
interceptors += client.interceptors
interceptors += RetryAndFollowUpInterceptor(client)
interceptors += BridgeInterceptor(client.cookieJar)
interceptors += CacheInterceptor(client.cache)
interceptors += ConnectInterceptor
if (!forWebSocket) {
interceptors += client.networkInterceptors
}
interceptors += CallServerInterceptor(forWebSocket)
复制代码
根据这段代码块我们可以知道拦截器链中按顺序排布了以下的拦截器:
- 应用拦截器:这是用户自定义的Interceptor,在初始化OkHttpClient的时候进行配置(也可以不配置),在拦截器链中的处理优先级最高,可以用于添加自定义header、自定义log配置、参数加密等;
- RetryAndFollowUpInterceptor:失败重试和重定向拦截器,这个拦截器的主要职责就是当请求失败的时候自动重试,并根据需要进行网络重定向;
- BridgeInterceptor:桥接拦截器,是连接应用层和网络层的桥梁,负责将用户的请求包装成服务端所需的请求,例如往请求头添加Content-Type、Content-Length、Host等等,并将服务端返回的响应转换成用户所需的响应,例如移除响应头中的Content-Encoding、Content-Length等等。如果响应用了gzip压缩过,则会对响应进行解压;
- CacheInterceptor:缓存拦截器,主要负责处理缓存中的请求和将服务器返回的Response写入缓存,如果命中强制缓存的话会直接返回Response而不会走后续的拦截器发起网络请求;
- ConnectInterceptor:连接拦截器,主要负责真正和服务器建立起连接;
- networkInterceptors:用户自定义的网络拦截器,本质上跟第一个拦截器一样,这个拦截器通常用于监控网络层的数据传输,可以按需配置;
- CallServerInterceptor:请求拦截器,在这个拦截器中真正发起了网络请求,并对服务器返回的Response进行解析,由于这是拦截器链中的最后一个拦截器,这里最终拿到Response之后就会直接将Response沿着拦截器链往回传送。
拦截器链
在构建完拦截器列表之后就是构建拦截器责任链RealInterceptorChain了,RealInterceptorChain实现了Interceptor.Chain接口,内部重写了Chain接口的所有方法,并定义了一个用于复制拦截器链的copy方法和维护了一个用于统计单个拦截器被调用次数的对象calls:
private var calls: Int = 0
internal fun copy(
index: Int = this.index,
exchange: Exchange? = this.exchange,
request: Request = this.request,
connectTimeoutMillis: Int = this.connectTimeoutMillis,
readTimeoutMillis: Int = this.readTimeoutMillis,
writeTimeoutMillis: Int = this.writeTimeoutMillis
) = RealInterceptorChain(call, interceptors, index, exchange, request, connectTimeoutMillis, readTimeoutMillis, writeTimeoutMillis)
复制代码
当然了RealInterceptorChain里面的方法我们不必每个都去细看,回顾一下在getResponseWithInterceptorChain方法里面是怎么拿到Response的:
val response = chain.proceed(originalRequest)
复制代码
Response是通过执行RealInterceptorChain的proceed方法来获取的,proceed方法也就是我们要重点分析的对象。
RealInterceptorChain.proceed方法
下面看看proceed方法的处理逻辑:
@Throws(IOException::class)
override fun proceed(request: Request): Response {
check(index < interceptors.size)
calls++
//①
if (exchange != null) {
check(exchange.finder.sameHostAndPort(request.url)) {
"network interceptor ${interceptors[index - 1]} must retain the same host and port"
}
check(calls == 1) {
"network interceptor ${interceptors[index - 1]} must call proceed() exactly once"
}
}
val next = copy(index = index + 1, request = request) //②
val interceptor = interceptors[index]
//③
@Suppress("USELESS_ELVIS")
val response = interceptor.intercept(next) ?: throw NullPointerException(
"interceptor $interceptor returned null")
//④
if (exchange != null) {
check(index + 1 >= interceptors.size || next.calls == 1) {
"network interceptor $interceptor must call proceed() exactly once"
}
}
check(response.body != null) { "interceptor $interceptor returned a response with no body" }
return response
}
复制代码
对这段代码逐步分析可以知道proceed方法做了以下的工作:
- 检查index的合法性
- 将当前拦截器调用proceed方法的次数calls递增(calls的初始值为0)
- ①处当exchange不为空时检查url和当前拦截器调用proceed方法的次数,其中exchange是对请求流的封装,在ConnectInterceptor进行拦截之前exchange为空,也就是说①处的代码块只有在执行ConnectInterceptor及其之后的拦截器时才会走进去,这时候需要主机和端口必须跟请求一开始创建时的一样,不允许之前的拦截器修改url,因为此时跟服务器的连接和请求流已经建立了,要是检测出已建立的连接不再支持当前url则会抛出异常。另外检查calls是为了限制ConnectInterceptor及其之后的拦截器最多只能执行一次proceed方法
- ②处调用copy方法复制创建出了新的拦截器链,copy的时候传入了index+1作为这条新拦截器链的index
- 取出拦截器列表中下标为index的拦截器,即当前拦截器
- ③处调用当前拦截器的intercept方法,将②处创建的新拦截器链传进去,并拿到Response,若拿不到Response则抛出异常
- ④处当exchange不为空时检查下一级拦截器的执行proceed方法次数,保证该拦截器至少要调用一次proceed方法,否则抛出异常
- 检查响应体的合法性,响应体为空则抛出异常,合法则返回Response
拦截器链如何递归
分析到这里可能还是会有点疑惑,单看proceed方法似乎只能看出来当前的拦截器被调用了,那是如何自动去执行下一个拦截器的呢?关键就是②和③处的代码了,现在已经知道③处执行当前拦截器的intercept方法时,会将②处构建的index为index+1的拦截器链传过去,可以推断出执行下一个拦截器的逻辑是在intercept方法中做的,以ConnectInterceptor为例我们点进去看看它的intercept方法干了什么:
object ConnectInterceptor : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
......//省略无关代码
return connectedChain.proceed(realChain.request)
}
}
复制代码
这段代码块中的connectedChain是一个RealInterceptorChain实例,可以看到在ConnectInterceptor的内部有调用到RealInterceptorChain的proceed方法,这不就又回到proceed里了嘛!因为刚刚传进来的RealInterceptorChain对象index往后移了一位,现在ConnectInterceptor完成了自身的处理回到proceed方法中,那将被执行的就是ConnectInterceptor的下一个拦截器了。
实际上,除了链中最后一个拦截器CallServerInterceptor之外每个拦截器的intercept方法里除了走自身的逻辑之外,都会有调用RealInterceptorChain的proceed方法,每个拦截器进入到proceed方法都会重新创建一个新的RealInterceptorChain对象,并且将index在当前的基础上加1,像这样经过RealInterceptorChain中的proceed方法和Interceptor中的intercept方法之间相互循环调用,当前拦截器完成拦截之后也就能自动执行下一个拦截器了,将Request沿着责任链层层传递下去,直到链中所有拦截器都完成拦截并将沿着责任链Response原路返回。当然了,如果当前拦截器处理完了不需要继续下一级的拦截,就可以将Response返回去,这时将不会调用RealInterceptorChain的proceed方法去启动下一级拦截器。如果在递归的过程中某个节点出现了异常,那这个节点之后的拦截器也不会被触发,除非重定向拦截器再次尝试新一轮的递归。
工作流程
下面是网络请求经过拦截器链的层层拦截后拿到Response的流程:
思考
1. 拦截器链遍历过程中为什么要不断copy新的chain?
在看RealInterceptorChain.proceed方法源码的时候不知道大家有没有这样的疑惑:在遍历拦截器链的时候为什么要不断去copy一个新的RealInterceptorChain对象去传给下一个拦截器呢?如果是单纯为了当前拦截器执行完后能够触发下一个拦截器让整条责任链自动运作起来,那只构建一个RealInterceptorChain对象每次传递时直接通过index++来实现不行吗?
上文已经详细分析过proceed方法,可以知道在proceed方法里会检查单个拦截器调用proceed的次数calls,正常情况下连接拦截器及其之后的拦截器最多只能proceed一次。如果说链中的每个拦截器都只会proceed一次的话用index++去实现是没有问题的,然而在实际中有的拦截器有可能会多次proceed,这时候它的calls就超过1了,如此一来单纯用index++就无法办到对特定的拦截器限制proceed次数了。
那为什么要用copy才可以办得到呢?这与OkHttp的重试机制有关,在重定向拦截器RetryAndFollowUpInterceptor内部有一个while循环,通过循环调用proceed方法来实现异常重试和网络重定向。比如说遍历拦截器链的过程中重定向拦截器之后的某一个节点出现了异常,要是这时候符合重试的条件,重定向拦截器就会再一次proceed。由于传递chain时每次都会copy一个新的,所以每个拦截器内部持有的chain对象都是自己特有的,不会受其他拦截器的影响,在异常重试的时候重定向拦截器会对自己的chain重新proceed,对后续的拦截器发起新一轮的遍历,随着不断重试proceed重定向拦截器自己的chain对象持有的calls也会随着递增下去,而其他拦截器的calls则不会受影响。
当然也由于会不断copy新的RealInterceptorChain对象,而且出现异常重试和网络重定向的情况时,重定向拦截器每proceed一次,其之后的节点都会重新copy出新的chain而不会复用重试之前的chain对象,如此一来当拦截器链中的节点过多的时候,性能也会受影响,这也是拦截器链的一个缺点。
2. 应用拦截器和网络拦截器有什么区别?
应用拦截器和网络拦截器都是由用户自定义的拦截器,本质上都是一样的,都是拦截器列表List<Interceptor>
,但是因为两者在拦截器链中的位置不同,导致这两个拦截器主要存在以下的区别:
- 触发次数:应用拦截器排布在拦截器链中的第一个位置,所以无论如何应用拦截器都一定会被触发,而且只会被触发一次。网络拦截器排布在ConnectInterceptor和CallServerInterceptor之间,在RetryAndFollowUpInterceptor和CacheInterceptor之后,如果走到CacheInterceptor这个节点命中了强制缓存,就会直接回溯不会继续走下去了,这种情况下网络拦截器是不会被触发的;如果在遍历拦截器链的过程中出现异常需要进行重试和重定向,RetryAndFollowUpInterceptor就会再次proceed对后续拦截器发起新一轮的遍历,相当于是进行二次请求,这种情况下网络拦截器则有可能被执行多次。也就是说网络拦截器未必会被触发,也可能会被多次触发。
- proceed方法调用次数:除了最后一个拦截器CallServerInterceptor不会调用RealInterceptorChain.proceed方法之外(因为其后面已经没有拦截器要被启动),剩下的拦截器理论上在一轮遍历中都应该至少执行一次proceed方法的。结合上文对proceed方法的分析,走到网络拦截器这个节点的时候跟服务器的连接已经建立起来了,所以网络拦截器每被触发一次可以且只能执行一次proceed方法。而实际上对于应用拦截器这个节点,当出现本地异常进行重试的情况可能会多次调用proceed方法。
- 应用场景:在遍历拦截器链的时候,因为应用拦截器只会被触发一次,所以可以用于统计应用的网络请求发起情况,而网络拦截器可能会被触发多次,被触发一次则代表发起了一次网络通信,所以可以用于监控网络链路上传输的数据。
PS:到这里本文对拦截器链的分析也结束了,关于重试机制、缓存机制等详细解析敬请关注后续文章,本文如有理解偏差的地方也欢迎各位朋友指出。