携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第8天,点击查看活动详情
前言
OkHttp源码系列文章:
- OkHttp源码之深度解析(一)——整体框架分析
- OkHttp源码之深度解析(二)——拦截器链详解:责任链模式
- OkHttp源码之深度解析(三)——分发器Dispatcher详解
- OkHttp源码之深度解析(四)——RetryAndFollowUpInterceptor详解:重试机制
- OkHttp源码之深度解析(五)——CacheInterceptor详解:缓存机制
在OkHttp源码之深度解析(一)中分析OkHttp整体框架时,对Dispatcher的职责作了简单的介绍。在OkHttp框架中,分发器Dispatcher负责调度Call,维护线程池和请求队列。对于同步请求和异步请求,Dispatcher的处理又有所不同,本文将详细分析Dispatcher是如何工作的。
PS:本文基于OkHttp3版本4.9.3
请求队列
在Dispatcher中维护了三个请求队列:
//已准备好的异步请求队列
private val readyAsyncCalls = ArrayDeque<AsyncCall>()
//正在执行的异步请求队列
private val runningAsyncCalls = ArrayDeque<AsyncCall>()
//正在执行的同步请求队列
private val runningSyncCalls = ArrayDeque<RealCall>()
复制代码
可以看到这三个请求队列都是ArrayDeque结构,使用双端队列的原因很好理解,因为这三个成员变量是用来存放网络请求的,网络请求的执行顺序需要按照先发起的先执行这个规则,就好比排队一样,新发起的请求插入在队尾,执行请求的时候取队头去执行,并把执行完的请求从队列头部移除。那么问题来了,链表也是可以实现这个功能的,那为什么不用链表呢?
在进行异步请求的时候,Dispatcher会遍历readyAsyncCalls查找符合的Call转移到runningAsyncCalls中(后文会具体说明),而链表中的元素在内存里是不连续的,查找元素需要遍历链表,相对比起来数组的查找效率会更高,因此采用ArrayDeque。
同步请求
同步请求的工作流程如下:
graph TB
start(发起同步请求)
runningSyncCallsAdd[请求加到runningSyncCalls队尾]
interceptorChain2[拦截器链]
finishRealCall[finish当前请求并将其从runningSyncCalls中移除]
isRunningSyncCallsEmpty{runningSyncCalls为空?}
finish(结束)
start-->runningSyncCallsAdd-->interceptorChain2-->finishRealCall-->isRunningSyncCallsEmpty--是-->finish
isRunningSyncCallsEmpty--否-->interceptorChain2
发起同步请求的时候,会调用到RealCall类的execute方法:
override fun execute(): Response {
......
try {
client.dispatcher.executed(this)
return getResponseWithInterceptorChain()
} finally {
client.dispatcher.finished(this)
}
}
复制代码
同步请求的逻辑很简单,请求发起的时候就会进入到Dispatcher的executed方法中:
@Synchronized internal fun executed(call: RealCall) {
runningSyncCalls.add(call)
}
复制代码
可以看到,Dispatcher只是将RealCall对象添加到正在执行的同步请求队列runningSyncCalls的队尾,在请求完成之后就会调用Dispatcher的finished方法结束请求(finished方法的具体处理逻辑会在后文详细分析)
异步请求
异步请求的工作流程如下:
流程图看上去很复杂,这里先对异步的流程有个整体的概念,下面会对流程的细节进行详细分析,等看完整个异步请求的分析回头再看这个流程图,相信会对Dispatcher的异步处理有清晰的认识。
异步请求时会调用到RealCall类的enqueue方法:
override fun enqueue(responseCallback: Callback) {
......
client.dispatcher.enqueue(AsyncCall(responseCallback))
}
复制代码
这时会构建一个AsyncCall对象并传给Dispatcher的enqueue方法:
internal fun enqueue(call: AsyncCall) {
synchronized(this) {
readyAsyncCalls.add(call)
if (!call.call.forWebSocket) {
val existingCall = findExistingCallWithHost(call.host)
if (existingCall != null) call.reuseCallsPerHostFrom(existingCall)
}
}
promoteAndExecute()
}
复制代码
Dispatcher将AsyncCall加到了readyAsyncCalls的队尾,并检测是否存在跟当前请求域名相同的请求,如果存在则复用,然后调用promoteAndExecute方法处理请求:
private fun promoteAndExecute(): Boolean {
this.assertThreadDoesntHoldLock()
val executableCalls = mutableListOf<AsyncCall>()
val isRunning: Boolean
synchronized(this) {
val i = readyAsyncCalls.iterator()
//遍历readyAsyncCalls
while (i.hasNext()) {
val asyncCall = i.next()
if (runningAsyncCalls.size >= this.maxRequests) break
if (asyncCall.callsPerHost.get() >= this.maxRequestsPerHost) continue
i.remove()
asyncCall.callsPerHost.incrementAndGet()
executableCalls.add(asyncCall)
runningAsyncCalls.add(asyncCall)
}
isRunning = runningCallsCount() > 0
}
//调用线程池去执行可执行的AsyncCall
for (i in 0 until executableCalls.size) {
val asyncCall = executableCalls[i]
asyncCall.executeOn(executorService)
}
return isRunning
}
复制代码
promoteAndExecute方法是Dispatcher处理异步请求的核心所在,主要做了以下的工作:
- 遍历readyAsyncCalls,对正在执行的异步请求数进行检测,如果超过最大限制数64则跳出循环,继续在等待队列readyAsyncCalls中等待处理;
- 如果正在执行的异步请求数小于64,继续对当前AsyncCall同一域名正在执行的请求数进行检测,如果超过最大限制数5则跳过当前请求,继续遍历readyAsyncCalls;
- 将符合条件的AsyncCall从等待队列readyAsyncCalls中取出来,并将其添加到正在执行队列runningAsyncCalls中,同时提交给线程池;
- 遍历完readyAsyncCalls之后遍历可执行请求集合executableCalls,调用线程池执行所有可执行的请求,即调用AsyncCall中的executeOn方法,因为AsyncCall实现了Runnable接口,所以在线程池中最终会调用AsyncCall.run()来执行异步请求;
- 返回是否有请求正在执行的标志位isRunning,其中正在执行的请求数通过runningCallsCount方法返回,统计了同步和异步的所有正在执行的请求:
@Synchronized fun runningCallsCount(): Int = runningAsyncCalls.size + runningSyncCalls.size 复制代码
查看promoteAndExecute方法的调用处可以发现,在设置maxRequests和maxRequestsPerHost时、enqueue方法以及finished方法中都会调用到,也就是当允许并行的最大请求数、同一个host允许并行的最大请求数发生变化时,或者有新的异步请求入队或请求完成后,都会重新调用线程池去处理任务。
为了帮助理解异步的处理流程这里举个栗子:假如现在有100个请求要同时进行异步,Dispatcher会将这100个请求先加入到等待队列中,然后再调用promoteAndExecute方法去按顺序遍历等待队列,挑出符合条件的64个请求移到runningAsyncCalls先处理,这时候runningAsyncCalls已经没有空位了,那么剩下的36个请求则继续留在等待队列里等待。当runningAsyncCalls中有请求已经完成了,Dispatcher就会把这个请求移出runningAsyncCalls,移除完成的请求之后现在runningAsyncCalls有空位了,Dispatcher就会重新遍历等待队列,从之前被跳过的36个请求中找到符合的请求加入runningAsyncCalls,每当有请求执行完都会调用promoteAndExecute方法重新遍历等待队列,确保这100个请求都能被执行。
在AsyncCall.run()执行完请求后,最终会执行Dispatcher的finished方法来结束请求任务,finished方法的具体处理逻辑接下来将详细分析。
请求结束
无论是同步请求还是异步请求,无论请求成功还是失败,在请求结束之后都会调用到Dispatcher的finished方法,只不过同步请求和异步请求进入的finished方法不同:
//用于异步请求
internal fun finished(call: AsyncCall) {
call.callsPerHost.decrementAndGet()
finished(runningAsyncCalls, call)
}
//用于同步请求
internal fun finished(call: RealCall) {
finished(runningSyncCalls, call)
}
复制代码
异步请求调用的finished方法在处理的时候多了call.callsPerHost.decrementAndGet()这一步,大概是为了将这个请求所在的host正在处理的请求数减一。最终同步和异步结束请求实际上都会执行以下的代码块:
private fun <T> finished(calls: Deque<T>, call: T) {
val idleCallback: Runnable?
synchronized(this) {
if (!calls.remove(call)) throw AssertionError("Call wasn't in-flight!")
idleCallback = this.idleCallback
}
val isRunning = promoteAndExecute()
if (!isRunning && idleCallback != null) {
idleCallback.run()
}
}
复制代码
这段代码块的逻辑很简单:
- 将当前请求从正在执行的同步/异步请求队列中移除
- 调用promoteAndExecute方法继续执行剩余的请求(包括所有剩余的同步请求和异步请求)
- 如果所有请求都已经执行完并且Dispatcher处于空闲状态,则执行空闲回调方法
结合这段代码块和上文对同步请求的分析可以发现,当Dispatcher处理同步请求的时候,只是在请求发起的时候将RealCall对象加入到runningSyncCalls中,到请求结束时将这个RealCall从runningSyncCalls中移除,整个处理的过程中并没有用到线程池,也没有限制并行的最大请求数,也就是说同步请求会在当前线程中被立即执行,而此时Dispatcher并没有体现出分发的功能,仅仅是对RealCall做了记录,真正能体现出Dispatcher的分发作用的是异步请求。
到这里本文对Dispatcher的分析也结束了,最后本文仅代表个人见解,如有理解偏差的地方欢迎各位朋友指出。