OkHttp 4.X 版本的解析 - 异步请求

333 阅读8分钟

前言

书接上文,在上一篇文档中,我们已经针对OkHttpClient通过同步的方式发送请求的逻辑进行了梳理,参考这里。这一篇文档,就是对于异步请求过程的分析。也会讲清楚,上一篇文章中提到的OkHttp官方说的最大同时并发的请求数64和同Host请求最大并发数5是如何实现的。

案例

httpClient.newCall(request).enqueue(object: Callback {
  override fun onFailure(call: Call, e: IOException) {
    TODO("Not yet implemented")
  }

  override fun onResponse(call: Call, response: Response) {
    TODO("Not yet implemented")
  }
})

Recall.enqueue()

OkHttpClient对象/Request对象/RealCall对象,就在这一章跳过了,我们直接看RealCall对象的enqueue方法的实现。在enqueue方法中,需要传入一个Callback对象,Callback是用于处理response对象的接口,实现类需要实现onFailureonResponse方法。下面就正式进入该方法的实现:

override fun enqueue(responseCallback: Callback) {
  check(executed.compareAndSet(false, true)) { "Already Executed" }

  callStart()
  client.dispatcher.enqueue(AsyncCall(responseCallback))
}

首先还是将类型为AtomicBoolean的executed标记为true,保证当前的RealCall对象只被执行一次。核心方法还是OkHttpClient对象的dispatcher来处理,并且向dispatcher.enqueue方法传入一个异步调用的Call对象。

AsyncCall

首先来看一看这个用于异步操作的类,这是一个属于RealCall类的内部类,提供了类似RealCall中对于请求处理的核心方法。在该类的最开始,我们就看到了一个很重要的属性,callsPerHost,这是一个AtomicInteger类型的变量,用于计数当前相同Host的请求正在执行的数量。在上一篇文章中我们提到过,OkHttp默认的实现中,只允许最多有5个来自相同Host的请求并发处理,而callsPerHost这个属性就保证了这个策略的执行。

@Volatile var callsPerHost = AtomicInteger(0)
  private set

Dispatcher

下面就又再次回到了Dispatcher类中,Dispatcher可以说是OkHttp的核心类,OkHttp的主要调度策略都是由该类来具体实现的。下面就开始分析其中的enqueue方法。

enqueue

internal fun enqueue(call: AsyncCall) {
  synchronized(this) {
    readyAsyncCalls.add(call)

    // Mutate the AsyncCall so that it shares the AtomicInteger of an existing running call to
    // the same host.
    if (!call.call.forWebSocket) {
      val existingCall = findExistingCallWithHost(call.host)
      if (existingCall != null) call.reuseCallsPerHostFrom(existingCall)
    }
  }
  promoteAndExecute()
}

首先就是要往保存已经就绪的异步调用任务的队列中保存数据,可以从下面的声明中看出,readyAsyncCalls集合依然是使用了跟上一篇文章中用于保存同步任务的队列相同的数据结构:ArrayDeque。这个数据结构在上一篇文章分析过,这是一个性能优于普通栈和链表的实现,但是非线程安全,因此,这里在往队列中添加数据的时候,需要使用synchronized关键词,来保证线程安全。注意:这里的add方法等同于addLast,因此这里是使用尾差的方法将新创建的AsyncCall对象添加到队列的队尾等待被执行。

private val readyAsyncCalls = ArrayDeque<AsyncCall>()

接着,我们的请求并不是一个webSocket类型的请求,因此,会进入if分支中。首先,会依次查询当前正在运行的异步任务队列和已经就绪的队列中是否已经存在相同host的请求,如果有,那么就返回这个已经存在的请求对象。这个已经存在的请求对象的作用,就是通过共享AsyncCall对象中的callsPerHost对象,以便于之后对于相同host请求的最大5个请求数量的控制。

if (existingCall != null) call.reuseCallsPerHostFrom(existingCall)

下面是AsyncCall类中该方法的实现:

fun reuseCallsPerHostFrom(other: AsyncCall) {
  this.callsPerHost = other.callsPerHost
}

接着就是Dispatcher类中非常重要的方法promoteAndExecute的调用。

promoteAndExecute

首先还是来看看官方对于这个方法的注释。这个方法就是将readyAsyncCalls队列中符合条件的请求对象放置到runningAsyncCalls队列中,完成请求状态变化的同时在线程池中开始执行该异步任务。并且官方标记,该方法不可以被放置在同步代码块中来执行,因为其中会涉及到调用者的代码。如果当前的dispatcher对象已经处于运行中状态,那么返回true。

首先第一行代码就是一个判断当前线程是否已经持有锁的断言,如果是,那么就会直接抛出异常,这个实现是最终通过调用Thread.holdsLock(this)来实现的

this.assertThreadDoesntHoldLock()

继续往下看,是一段使用了synchronized实现的同步代码块,原因还是,存储异步任务的2个队列都是线程不安全的数据结构。

synchronized(this) {
  val i = readyAsyncCalls.iterator()
  while (i.hasNext()) {
    val asyncCall = i.next()

    if (runningAsyncCalls.size >= this.maxRequests) break // Max capacity.
    if (asyncCall.callsPerHost.get() >= this.maxRequestsPerHost) continue // Host max capacity.

    i.remove()
    asyncCall.callsPerHost.incrementAndGet()
    executableCalls.add(asyncCall)
    runningAsyncCalls.add(asyncCall)
  }
  isRunning = runningCallsCount() > 0
}

首先依次取出readyAsyncCalls队列中的任务,即:已经准备好的等待被执行的任务。接着就是判断当前runningAsyncCalls队列的长度是否已经超过OkHttp定义的最大支持64个异步任务的数量限制,如果已经有64个任务在执行中了,那么就不再开始新任务,方法直接结束。然后就是对相同host请求的限制,这里就从AsyncCall对象的callsPerHost属性中取得当前执行的数量,如果已经有5个相同host的任务在执行了,那么结束本次循环,继续取出下一个任务。

处理完了对于并发的限制,下面的逻辑就是当前的任务符合继续执行的条件了,可以将该任务从准备完毕状态转换到执行中的状态。首先就是要调用当前的异步任务对象的callsPerHost属性的incrementAndGet方法,实现该AtomicInteger的自增。这里再次说明该变量使用了CAS的方式来保证线程安全。这一轮操作以后,isRunning的状态将被设置为true,即:dispatcher处于运行中的状态。同时这里也要注意,dispatcher要解除运行中状态需要runningAsyncCallsrunningSyncCalls都没有可执行任务。

@Synchronized fun runningCallsCount(): Int = runningAsyncCalls.size + runningSyncCalls.size

这里需要注意,只要任务满足被执行的条件,那么就会被首先从readyAsyncCalls队列中被移除,这里就解释了为什么OkHttp说每一个任务只可以被执行一次。

再接着往下走,就该通过线程池来启动可以被执行的任务了。

for (i in 0 until executableCalls.size) {
  val asyncCall = executableCalls[i]
  asyncCall.executeOn(executorService)
}

最终的实现又再次回到了AsyncCall对象的executeOn方法,我们接着分析。

AsyncCall

executeOn

首先还是来看看官方解释,该方法在启动线程池来执行任务的时候,如果线程池对象已经被shutdown了,那么就会进行对象的清理。

fun executeOn(executorService: ExecutorService) {
  client.dispatcher.assertThreadDoesntHoldLock()

  var success = false
  try {
    executorService.execute(this)
    success = true
  } catch (e: RejectedExecutionException) {
    val ioException = InterruptedIOException("executor rejected")
    ioException.initCause(e)
    noMoreExchanges(ioException)
    responseCallback.onFailure(this@RealCall, ioException)
  } finally {
    if (!success) {
      client.dispatcher.finished(this) // This call is no longer running!
    }
  }
}

可以看到,在方法的一开始,依然要保证该方法是不允许在已经持有锁的线程运行的。核心代码就只有一句,就是通过executorService.execute(this)方法直接在线程池中执行该任务。

任务执行失败

如果这里被线程池给拒绝了,那么就会将这个异常回调给调用者。调用者的onFailure回调会触发。而如果任务启动失败,那么dispatcher的finished方法就会被调用,我们先来分析这个失败的场景处理,因此我们又要回到Dispatcher类中。

Dispatcher

finished

internal fun finished(call: AsyncCall) {
  call.callsPerHost.decrementAndGet()
  finished(runningAsyncCalls, call)
}

首先就是将同Host的运行中数量-1,这里也是CAS的操作,就不再提了。然后继续跟进,

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()
  }
}

这个方法我们在同步任务执行中也见过,总共做2个事情,第一件事情是从对应的执行中队列将当前对象移除,这里传入的是runningAsyncCalls的异步任务队列。并且再次调用promoteAndExecute方法来处理下一个新任务。第二个事情就是在需要的情况下调用idleCallback回调。至此,任务执行失败的情形就分析完毕。下面我们继续回到线程池执行任务的地方,看看异步任务是如何在线程池中被执行的。

AsyncCall:Runnable

我们知道,线程池对象executorService.execute方法的参数需要一个Runnable对象,因此,AsyncCall必然是继承自Runnable,具体的实现必然在run方法中,代码如下。

run

override fun run() {
  threadName("OkHttp ${redactedUrl()}") {
    var signalledCallback = false
    timeout.enter()
    try {
      val response = getResponseWithInterceptorChain()
      signalledCallback = true
      responseCallback.onResponse(this@RealCall, response)
    } catch (e: IOException) {
      if (signalledCallback) {
        // Do not signal the callback twice!
        Platform.get().log("Callback failure for ${toLoggableString()}", Platform.INFO, e)
      } else {
        responseCallback.onFailure(this@RealCall, e)
      }
    } catch (t: Throwable) {
      cancel()
      if (!signalledCallback) {
        val canceledException = IOException("canceled due to $t")
        canceledException.addSuppressed(t)
        responseCallback.onFailure(this@RealCall, canceledException)
      }
      throw t
    } finally {
      client.dispatcher.finished(this)
    }
  }
 }
}

这里在执行每一个任务的时候,会给这个线程一个跟当前请求URL相关的名称,并且在完成以后将线程名修改回去。下面就是真正任务的执行了,同样是先开启请求超时的计时。然后跟同步请求的执行一样,调用getResponseWithInterceptorChain方法来通过所有的拦截器,最终获得请求的response对象。这里就不再展开了。

val response = getResponseWithInterceptorChain()
signalledCallback = true
responseCallback.onResponse(this@RealCall, response)

response成功返回以后,调用者的onResponse方法被回调。如果这里报错了,那么OkHttp会把该任务取消掉,取消的实现如下:

override fun cancel() {
  if (canceled) return // Already canceled.

  canceled = true
  exchange?.cancel()
  connectionToCancel?.cancel()

  eventListener.canceled(this)
}

在该方法中,当前的socket连接也会被取消。

至此,我们分析完了异步请求的完整执行过程。