OkHttp源码之深度解析(三)——分发器Dispatcher详解

OkHttp源码之深度解析(三)——分发器Dispatcher详解

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第8天,点击查看活动详情

前言

OkHttp源码系列文章:

在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异步请求流程.png 流程图看上去很复杂,这里先对异步的流程有个整体的概念,下面会对流程的细节进行详细分析,等看完整个异步请求的分析回头再看这个流程图,相信会对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的分析也结束了,最后本文仅代表个人见解,如有理解偏差的地方欢迎各位朋友指出。

分类:
Android
标签: