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

817 阅读7分钟

OkHttp获取网络数据的案例

val httpClient = OkHttpClient.Builder()
  .addNetworkInterceptor(LogInterceptor())
  .addNetworkInterceptor(SoftInterceptor())
  .build()
val request = Request.Builder().url("https://www.baidu.com").build()

val response = httpClient.newCall(request).execute()
response.body?.close()

OkHttpClient的官方最佳实践

创建

来自于官方对于OkHttpClient的官方注释,我们应该使用单例的方式来使用OkHttpClient。原因就是,每一个OkHttpClient都会持有属于对象自己的连接池线程池,共享OkHttpClient对象可以降低延迟以及减少内存的使用。

创建OkHttpClient的方式主要有3种。可以直接通过new的方式,

public final OkHttpClient client = new OkHttpClient();

也可以使用Builder,

public final OkHttpClient client = new OkHttpClient.Builder()
    .addInterceptor(new HttpLoggingInterceptor())
    .cache(new Cache(cacheDir, cacheSize))
    .build();

或者也可以基于已有的client对象的newBuilder()方法来获得一个新对象,

OkHttpClient eagerClient = client.newBuilder()
    .readTimeout(500, TimeUnit.MILLISECONDS)
    .build();

这个方式会在稍后的部分稍加分析。

关闭

官方注释上说,对于client来说,并不需要刻意去手动调用shutdown方法来释放被持有的连接和线程,因为这些资源如果处于idle状态下,将会被自动关闭和释放。OkHttpClient还是提供了shutdown方法来供开发者调用,直接关闭和释放资源。在关闭的时候,还处于待执行的call也会被同时拒绝掉。

client.dispatcher().executorService().shutdown();

可以调用evictAll()方法来清空client连接池中的对象。这里要注意的是,连接池对象的守护线程并不会立刻退出,所以这里是一个异步的调用。

client.connectionPool().evictAll();

通过调用close方法来清空缓存。这里需要注意的是如果说是基于一个已经被关闭的缓存对象来创建新的请求,那么就会报错。

OkHttp针对HTTP/2使用了守护线。这些线程也会在处于idle状态的时候被自动的回收。

newCall()

newCall的作用就是生成一个可以被执行的Call对象,而Call是一个抽象定义的接口,是OkHttp中用来描述一个已经做好了被执行的准备的对象。这个对象就代表了一次请求的请求过程和响应过程。这里需要注意的是,Call对象是不允许被执行2次的。

回到httpClient调用newCall这里,这里实际上是返回的Call的实现类RealCall的对象

override fun newCall(request: Request): Call = RealCall(this, request, forWebSocket = false)

RealCall

首先还是来自于官方的解释,这个对象是介于应用层和网络层的桥梁。该类支持被异步取消。接下来就顺着这个构造方法,第一个参数this指向了我们生成的OkHttpClient对象,第二个request对象在RealCall中指向的是originalRequest。官方的解释是,这个对象是一个来自于应用调用者的纯净的对象,不包含重定向或者授权认证的信息。

Request

创建

可以从上面的示例中看到,Request同样是通过Builder的方式来构建对象。在我们的案例中,由于只是简单的使用了url方法来配置要访问的url地址,因此就暂时跳过这一部分的分析。在url的set方法中,Request类其实帮我们处理了对于webSocketwebSocket Secure的处理。

open fun url(url: String): Builder {
  // Silently replace web socket URLs with HTTP URLs.
  val finalUrl: String = when {
    url.startsWith("ws:", ignoreCase = true) -> {
      "http:${url.substring(3)}"
    }
    url.startsWith("wss:", ignoreCase = true) -> {
      "https:${url.substring(4)}"
    }
    else -> url
  }

  return url(finalUrl.toHttpUrl())
}

RealCall().execute()

官方说明

调用这个方法之后,这个请求将会被立刻执行,并且会阻塞线程直到服务器端返回可以被调用者使用的响应或者该请求失败。为了避免资源泄露,调用者应当在访问结束以后,及时关闭Response对象,进而会依次也将response中的ResponseBody对象关闭。从response.close()的实现来看,本质上就是调用了responseBody.close()。

方法实现

在方法的最开始,会先将RealCall对象中标记当前请求是否被执行过的标记属性executed设置为true。

private val executed = AtomicBoolean()
executed.compareAndSet(false, true)

这里可以看到,OkHttp在这里使用到了原子变量来解决多线程冲突的问题,executed属性使用了CAS的方式来保证线程安全。

接着,通过check方法,保证了每一个call对象只会被执行一次。然后就是调用timeout.enter()开始处理请求超时的相关场景。下面是timeout对象的初始化代码,timeout对象本身是一个匿名的AsyncTimeout对象,这个对象会运行在另外的独立线程中,这里先不深入。当时间满足我们传入的超时时间以后,timeout对象的timedOut()方法就会被调用。从下面的代码可以看出,超时时间的设置来自于OkHttpClient.Builder对象的callTimeout属性。而超时以后,会调用RealCall对象的cancel()方法,将本次访问取消。取消的部分稍后解析。

private val timeout = object : AsyncTimeout() {
  override fun timedOut() {
    cancel()
  }
}.apply {
  timeout(client.callTimeoutMillis.toLong(), MILLISECONDS)
}

接着就会调用用于监听每一个Call事件方法调用的生命周期的事件函数,我们可以通过OkHttpClient.Build.eventListenerFactory来设置一个自定义的监听对象,默认实现是一个空实现。

eventListener.callStart(this)

接着,就到了真正发起请求的位置了,可以看到,是OkHttpClient对象中的dispatcher最终来执行了这个操作。下面就进入到这个Dispatcher类中,看一看真正发起请求的实现是如何的。可以看到,dispatcher对象也是来自于OkHttpClient在初始化的时候一起初始化的属性。

@get:JvmName("dispatcher") val dispatcher: Dispatcher = builder.dispatcher

client.dispatcher.executed(this)

Dispatcher

官方说明

Dispatcher决定了我们的异步请求是如何被调度和执行的。每一个dispatcher对象内部都会通过ExecutorService也就是线程池技术来运行网络请求。这个线程池是支持调用者自定义的,但是官方这里提出,如果调用者使用了自定义的线程池,那么应该能够拥有至少支持OkHttp官方支持的最多64个请求并行执行的能力。

OkHttp的并发能力

上面的描述也就顺带引出了OkHttp官方为我们提供的并发能力配置,具体的配置是由下面的属性描述的,可以看到,当前支持的最大请求数量为64个。也可以看到,这里OkHttp官方是使用了Synchronized来保证线程安全。

@get:Synchronized var maxRequests = 64
  set(maxRequests) {
    require(maxRequests >= 1) { "max < 1: $maxRequests" }
    synchronized(this) {
      field = maxRequests
    }
    promoteAndExecute()
  }

从该变量的官方说明可以看到,这里的64,指的是当前可以被同时执行的请求的最大数量,这一点我们可以在后续的线程池配置上加以佐证。如果当前请求的总数量超过了64个,那么多出来的请求将会处于等待状态,等待有资源的时候才会被执行。

接着,对于来自不同Host的请求,也存在着一个针对相同Host的并发数量限制,5。

@get:Synchronized var maxRequestsPerHost = 5
    set(maxRequestsPerHost) {
      require(maxRequestsPerHost >= 1) { "max < 1: $maxRequestsPerHost" }
      synchronized(this) {
        field = maxRequestsPerHost
      }
      promoteAndExecute()
    }

这里官方对其的描述为,这个数字代表的是同一时间能够被同时执行的相同Host的请求数量。这个限制是通过url的host部分来完成的,因此对于相同的IP地址,并发的数量可能会超过这个数量限制。这个也容易理解,不同的url地址可能最终会解析到同样的IP地址上面去。如果已经超过了当前同Host地址的请求数量,那么超过的请求将会先进行等待。wswebSocket类型的连接不受这个参数的控制。

线程池

接着我们就来看看Dispatcher中最重要的线程池的实现。

 @get:Synchronized
  @get:JvmName("executorService") val executorService: ExecutorService
    get() {
      if (executorServiceOrNull == null) {
        executorServiceOrNull = ThreadPoolExecutor(0, Int.MAX_VALUE, 60, TimeUnit.SECONDS,
            SynchronousQueue(), threadFactory("$okHttpName Dispatcher", false))
      }
      return executorServiceOrNull!!
    }

首先可以看到,这里是使用了懒加载的方式来初始化线程池的,同样使用了Synchronized关键词来保证线程安全。接着我们就来分析一下这个线程池的配置参数。

可以看到,核心线程数量为0,不限制线程池中的线程数量,线程池中每一个线程对象如果超过60秒都还没有活干的话,那么就会被终止(terminate)。然后在要使用的阻塞队列这里,使用的是SynchronousQueue这个数据结构。这是一个很有意思的数据结构。详情可以查看www.jb51.net/article/221… 的说明。这个阻塞队列的核心特征就是没有size,当我们往队列中put元素之后,当前线程就会被阻塞,直到有别的线程调用take方法将该元素取走,下一次put才会有机会执行。为什么OkHttp团队要选择这个数据结构作为线程池的任务容器,我这里只能做一些揣测。该队列中不会存储任何的元素信息,有元素那么就会在最短的时间被消费者消费,不需要考虑数据恢复问题。最后就是新线程的生成策略了。这是一个很简单的工厂方法,如下:

fun threadFactory(
  name: String,
  daemon: Boolean
): ThreadFactory = ThreadFactory { runnable ->
  Thread(runnable, name).apply {
    isDaemon = daemon
  }
}

只是给线程命了名并且制定非守护线程而已。

dispatcher.execute()

代码如下,只有一句,就是将RealCall对象添加到runningSyncCall的一个双端队列中。

// 包含了所有正在执行中的请求对象,包括已经被取消的请求单还未执行完毕的
private val runningSyncCalls = ArrayDeque<RealCall>()

@Synchronized internal fun executed(call: RealCall) {
  runningSyncCalls.add(call)
}

简单的说明一下ArrayDeque这个数据结构的特点:

  1. 这是一个基于可变数组实现的双端队列
  2. 线程不安全,因此这里在添加元素的时候,需要在execute方法上使用@Synchronized来保证线程安全
  3. 被用作栈的时候速度比Stack快;被用作链表的时候速度比LinkedList快

需要被立即执行的RealCall对象最终被添加到runningSyncCall队列中以后,execute的部分就结束了。那么这个被添加的对象是什么时候被执行的呢?接下来就要回到RealCall对象的execute方法中了,最后一步会调用方法getResponseWithInterceptorChain()真正发起请求,并且返回Response对象。

getResponseWithInterceptorChain

首先会添加所有调用者添加的适配器,和OkHttp官方为我们生成的一系列拦截器。这里需要注意的是,如果不是webSocket类型的请求的话,会把调用者配置的networkInterceptors一并添加到拦截器集合中。

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)

然后,会构造出真正的chain对象,类型是RealInterceptorChain,通过调用chain对象的proceed方法,返回response对象。

val response = chain.proceed(originalRequest)

chain.proceed

这个方法的关键点就在于对于拦截器的依次调用和处理,这是典型的责任链设计模式的应用,并且是基于递归实现的责任链调度,代码如下:

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")

递归的方法就是我们这里的proceed方法。首先会获取到下一个要被执行的RealInterceptorChain对象,这是调用的copy方法,实际是构造了一个新的RealInterceptorChain对象,只是index会增加1。然后会获取到当前需要执行的interceptor对象,通过调用interceptor对象的intercept方法,将下一个将要执行的对象传入。因此在实现我们自己的拦截器的时候,我们需要通过调用这个传进来的形参获得response对象,这就实现了拦截器的递归调用。那么递归的重点在哪里呢。就是在上面添加拦截器的时候我们看到的最后一个拦截器CallServerInterceptor,在这个对象的intecept方法中,就没有调用proceed方法了,因此,递归中断,开始回溯。这也能看出,调用者的拦截器最先被执行,最后才被终止。CallServerInterceptor不在本次讨论范围内,就跳过了。

至此,我们就已经拿到了从服务端获取到的http请求的响应,并且封装为resposne对象返回。最后一步,就是一些收尾工作。而这个收尾工作依旧需要回到dispatcher对象的finished方法中。

dispatcher.finished()

在该方法中,会对发起请求时申请的一些资源进行处理,方法实现如下:

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

首先就是从runningSyncCalls队列中将当前的已经访问过的call对象移除,前面提到过,由于这个队列是线程不安全的,因此在操作的时候需要用synchronized关键词来保证线程安全。最后,调用promoteAndExecute方法,决定是否需要在所有任务都执行完毕以后运行idleCallback回调函数。这个函数会根据当前几个队列中的任务状况,来判断当前的dispatcher对象是否处于运行中的状态。由于我们当前分析的是同步调用的过程,因此这个方法中的isRunning就会返回false,代表我们的所有访问都已经结束了。

至此,同步调用的整个过程我们就分析完了。

client.newBuilder(anotherClientInstance)

这个方法就是重载了Builder的构造器,基于传入的对象,生成一个新的OkHttpClient.Builder对象。

client.dispatcher().executorService().shutdown()

这个就是直接调用了dispatcher对象中持有的线程连接池的shutdown方法。这就回到了关于线程池的状态管理了,当该方法被调用以后,线程池将停止接受新的任务,并且在所有已经被提交的任务执行完毕以后,将线程池关闭。