Android进阶宝典 -- OkHttp3源码分析(Kotlin版本分发器和拦截器)

一个项目稳定运行,离不开基础能力建设,也就是我们常说的基础库,常见的有网络库、图片加载库、日志库、埋点库等等,所有的项目都会依赖这些基础库,因此基础库的迭代准则就是:改动不得影响上层业务的使用,架构设计最基本的开闭原则,那么这一篇文章中,我将会从网络基础库入手,从OkHttp到Retrofit源码开始讲起。

在此之前,如果熟悉OkHttp的伙伴,大概应该知道OkHttp作为Android最热门的网络请求框架,其自身的优势在于:

(1)内部实现Socket连接池,减少了延迟请求;

我们知道,socket是应用层的协议,传输层采用的是TCP/IP协议,如果每次请求都去和服务端建立一次连接,响应完成之后断开连接,势必影响到了请求的时效性,因此OkHttp内部通过缓存socket连接池,可复用socket,减少3次握手和4次挥手的操作,以达到快速响应。

(2)使用GZip压缩数据;

使用GZip能够最大化的压缩数据,数据packet减少能够提高传输效率。

(3)缓存响应数据,避免重复的网络请求;

当客户端发起一次接口请求之后,例如xxx/api/users,此时服务端会返回对应的响应response,此时OkHttp会选择将这些响应缓存起来(具体能否缓存取决于数据本身),当下次客户端发起同样的请求之后,就不需要过服务端,直接取本地的缓存响应返还给客户端。

(4)请求失败自动重定向

当客户端一次请求失败之后,OkHttp会自动重试主机的其他ip。

上述这么多的优势,我们均可以从源码中找到端倪。

1 OkHttp的基础使用

对于网络请求,我们常用的就是get和post请求,在OkHttp中,通过Request创建两种不同的请求方式,利用OkHttpClient,可以看作是客户端发起请求(newCall),最终获取服务端的响应结果。

Kotlin 复制代码
init {
    client = OkHttpClient.Builder()
        .build()
}

fun get(url: String): String? {
    //创建GET请求
    val request = Request.Builder()
        .url(url)
        .build()
    val response = client.newCall(request).execute()
    return response.body?.string()
}

fun post(url: String, body: RequestBody): String? {
    //创建POST请求
    val request = Request.Builder()
        .url(url)
        .post(body)
        .build()
    val response = client.newCall(request).execute()
    return response.body?.string()
}

具体的请求流程为:

我们可以看到,当OkHttp进行请求时,有两种方式:execute和enqueue,分别代表同步和异步请求,在上面代码中是同步请求的实现方式,会阻塞线程直到响应返回。

当然并不是创建完请求就直接拿到结果,中间涉及到了分发器和拦截器的处理逻辑:

(1)分发器:内部维护了队列和线程池,完成请求的分发;

为什么要使用线程池,其实很简单,如果只是一个单线程的框架,那么每次请求都需要判断当前是否有任务在执行,如果有那么就需要排队,显然不符合网络请求的场景,当有多个任务来的时候,需要线程调度并发处理。

(2)拦截器:OkHttp的核心组件,默认有5大拦截器

1.1 enqueue方法分析

对于OkHttp的同步和异步方法,我们先分析异步方法,因为两者有共同之处,首先当我们创建一个请求对象之后,会调用newCall方法。

kotlin 复制代码
/** Prepares the [request] to be executed at some point in the future. */
override fun newCall(request: Request): Call {
  return RealCall.newRealCall(this, request, forWebSocket = false)
}

通过调用client的newCall方法,我们看是以创建的Request为参数,最终返回了一个RealCall对象。

kotlin 复制代码
companion object {
  fun newRealCall(
    client: OkHttpClient,
    originalRequest: Request,
    forWebSocket: Boolean
  ): RealCall {
    // Safely publish the Call instance to the EventListener.
    return RealCall(client, originalRequest, forWebSocket).apply {
      transmitter = Transmitter(client, this)
    }
  }
}

那么最终调用enqueue或者execute方法,都是调用了RealCall对象的方法。

kotlin 复制代码
override fun enqueue(responseCallback: Callback) {
  synchronized(this) {
    check(!executed) { "Already Executed" }
    executed = true
  }
  //这里会进行事件的回调
  transmitter.callStart()
  client.dispatcher.enqueue(AsyncCall(responseCallback))
}

开始会做一次同步判断验证,当执行一次enqueue方法时,会将executed置为true,如果多次调用enqueue方法,那么就会直接抛出异常。

接下来的核心方法就是,调用了Dispatcher的enqueue方法,在创建OkHttpClient时,可以将自定义的Dispatcher对象设置进去,否则将会使用默认的Dispatcher分发器。

kotlin 复制代码
// OkHttpClient.Builder 构造函数

class Builder constructor() {
  
  internal var dispatcher: Dispatcher = Dispatcher()
}

1.1.1 Dispatcher分发器

因为作为异步方法,对于结果的返回是通过回调通知客户端,因此调用RealCall的enqueue方法时,会设置一个Callback对象。

kotlin 复制代码
interface Callback {
  /**
   * Called when the request could not be executed due to cancellation, a connectivity problem or
   * timeout. Because networks can fail during an exchange, it is possible that the remote server
   * accepted the request before the failure.
   */
  fun onFailure(call: Call, e: IOException)

  /**
   * Called when the HTTP response was successfully returned by the remote server. The callback may
   * proceed to read the response body with [Response.body]. The response is still live until its
   * response body is [closed][ResponseBody]. The recipient of the callback may consume the response
   * body on another thread.
   *
   * Note that transport-layer success (receiving a HTTP response code, headers and body) does not
   * necessarily indicate application-layer success: `response` may still indicate an unhappy HTTP
   * response code like 404 or 500.
   */
  @Throws(IOException::class)
  fun onResponse(call: Call, response: Response)
}

当请求成功之后,会在onResponse方法中回调响应对象,那么在交由Dispatcher处理时,会将Callback对象封装成AsyncCall对象。

Dispatcher # enqueue

kotlin 复制代码
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.get().forWebSocket) {
      val existingCall = findExistingCallWithHost(call.host())
      if (existingCall != null) call.reuseCallsPerHostFrom(existingCall)
    }
  }
  promoteAndExecute()
}

Dispatcher # enqueue是一个同步方法,首先会将AsyncCall对象加入到readyAsyncCalls队列中。在Dispatcher中,有3个重要的队列需要知道:

kotlin 复制代码
/** Ready async calls in the order they'll be run. */
private val readyAsyncCalls = ArrayDeque<AsyncCall>()

/** Running asynchronous calls. Includes canceled calls that haven't finished yet. */
private val runningAsyncCalls = ArrayDeque<AsyncCall>()

/** Running synchronous calls. Includes canceled calls that haven't finished yet. */
private val runningSyncCalls = ArrayDeque<RealCall>()

readyAsyncCalls队列是等待任务队列,存储已经准备好的任务,可以随时执行的;runningAsyncCalls队列是正在执行的异步任务队列,同时也包括正在取消的任务;runningSyncCalls队列是正在执行的同步任务队列。

OKHttp在4.0之后和4.0之前还是有区别的,在4.0之前(java版本)中,在执行enqueue方法时,Dispatcher是会判断将异步任务加入到ready还是running队列中。而在4.0之后(kotlin版本)中,统一都是加入到了ready队列中。

接下来会判断当前请求是否为websocket请求,如果是,那么不做任何处理;如果不是,那么会调用findExistingCallWithHost方法,

kotlin 复制代码
private fun findExistingCallWithHost(host: String): AsyncCall? {
  for (existingCall in runningAsyncCalls) {
    if (existingCall.host() == host) return existingCall
  }
  for (existingCall in readyAsyncCalls) {
    if (existingCall.host() == host) return existingCall
  }
  return null
}

runningAsyncCallsreadyAsyncCalls两个队列中,查找是否存在相同域名的请求,如果存在,那么就返回,同时修改当前AsyncCall对象的callsPerHost参数。

kotlin 复制代码
fun callsPerHost(): AtomicInteger = callsPerHost

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

为什么要统计这个数字,是因为OKHttp对于请求是有限制,在Dispatcher中有两个参数分别为:maxRequestsmaxRequestsPerHost,代表最大请求数和同一域名下的最大请求数,超出这个数字将不会继续工作。

kotlin 复制代码
/**
 * The maximum number of requests to execute concurrently. Above this requests queue in memory,
 * waiting for the running calls to complete.
 *
 * If more than [maxRequests] requests are in flight when this is invoked, those requests will
 * remain in flight.
 */
@get:Synchronized var maxRequests = 64
  set(maxRequests) {
    require(maxRequests >= 1) { "max < 1: $maxRequests" }
    synchronized(this) {
      field = maxRequests
    }
    promoteAndExecute()
  }

/**
 * The maximum number of requests for each host to execute concurrently. This limits requests by
 * the URL's host name. Note that concurrent requests to a single IP address may still exceed this
 * limit: multiple hostnames may share an IP address or be routed through the same HTTP proxy.
 *
 * If more than [maxRequestsPerHost] requests are in flight when this is invoked, those requests
 * will remain in flight.
 *
 * WebSocket connections to hosts **do not** count against this limit.
 */
@get:Synchronized var maxRequestsPerHost = 5
  set(maxRequestsPerHost) {
    require(maxRequestsPerHost >= 1) { "max < 1: $maxRequestsPerHost" }
    synchronized(this) {
      field = maxRequestsPerHost
    }
    promoteAndExecute()
  }

当碰到这两个参数时,我们再做解释,接下来会调用promoteAndExecute方法。

Dispatcher # promoteAndExecute

kotlin 复制代码
/**
 * Promotes eligible calls from [readyAsyncCalls] to [runningAsyncCalls] and runs them on the
 * executor service. Must not be called with synchronization because executing calls can call
 * into user code.
 *
 * @return true if the dispatcher is currently running calls.
 */
private fun promoteAndExecute(): Boolean {
  assert(!Thread.holdsLock(this))

  val executableCalls = mutableListOf<AsyncCall>()
  val isRunning: Boolean
  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
  }

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

  return isRunning
}

在这个方法中,会从readyAsyncCalls队列中,选择符合条件的异步任务放入runningAsyncCalls队列中,以便在线程池中执行。

所以在这个方法中,会遍历readyAsyncCalls队列:

(1)如果runningAsyncCalls队列的size已经大于最大请求数,那么就直接跳出遍历;

(2)如果当前请求已经超过了同一域名下最大的请求数,那么就跳过遍历下一个任务;

所以只要都不满足上述两个条件,那么就会将此任务:

(1)从readyAsyncCalls队列中移除;

(2)将此任务的callsPerHost参数加1,便于为后边的任务提供maxRequestsPerHost条件判断;

(3)将此任务加入到runningAsyncCalls队列中。

选出符合条件的任务之后,执行每个任务的executeOn方法,这里会进入到线程池中。

这里注意一下,promoteAndExecute方法是有返回值的,isRunning是通过判断runningCallsCount是否大于0,即runningAsyncCallsrunningSyncCalls两个队列的任务数是否大于0.

kotlin 复制代码
@Synchronized fun runningCallsCount(): Int = runningAsyncCalls.size + runningSyncCalls.size

1.1.2 AsyncCall的执行

通过Dispatcher挑选出符合条件的AsyncCall之后,会调用executeOn方法,这里会传入创建的线程池,这个线程池有什么特点呢?

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

核心线程数为0,最大线程数无限大,这也意味着可以无限创建线程,支持高并发保证最大的吞吐量;其次创建线程池需要队列,SynchronousQueue也是BlokingQueue,但是为什么不选择LinkedBlockingQueue或者ArrayBlockingQueue,是因为SynchronousQueue会比另外两种有更高的吞吐量。

在executeOn方法中,就会直接执行AsyncCall自身对象,那么说明其实现了Runnable接口,如果执行成功了,那么就将success设置为true。

kotlin 复制代码
fun executeOn(executorService: ExecutorService) {
  assert(!Thread.holdsLock(client.dispatcher))
  var success = false
  try {
    executorService.execute(this)
    success = true
  } catch (e: RejectedExecutionException) {
    val ioException = InterruptedIOException("executor rejected")
    ioException.initCause(e)
    transmitter.noMoreExchanges(ioException)
    responseCallback.onFailure(this@RealCall, ioException)
  } finally {
    if (!success) {
      client.dispatcher.finished(this) // This call is no longer running!
    }
  }
}

AsyncCall # run

kotlin 复制代码
override fun run() {
  threadName("OkHttp ${redactedUrl()}") {
    var signalledCallback = false
    transmitter.timeoutEnter()
    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(INFO, "Callback failure for ${toLoggableString()}", e)
      } else {
        responseCallback.onFailure(this@RealCall, e)
      }
    } finally {
      client.dispatcher.finished(this)
    }
  }
}

那么看到这里,整个请求的基本流程算是接近尾声了,因为在调用getResponseWithInterceptorChain方法之后,返回了一个Response对象,并通过Callback回调onResponse方法将请求返回客户端;如果失败,那么就调用onFailure方法。

最终请求完成之后,调用了Dispatcher的finished方法。

Dispatcher # finished

kotlin 复制代码
internal fun finished(call: AsyncCall) {
  call.callsPerHost().decrementAndGet()
  finished(runningAsyncCalls, call)
}

/** Used by `Call#execute` to signal completion. */
internal fun finished(call: RealCall) {
  finished(runningSyncCalls, call)
}

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

其实finished方法主要就是做一些状态同步:

(1)对于当前任务的callsPerHost参数减1;

(2)从runningSyncCalls队列中移除这个任务;

(3)有旧的任务移除,意味着等待队列中的任务有机会被执行,所以又再次调用了promoteAndExecute方法,进行新的一轮任务筛选。

1.1.3 拦截器分析

前面我们讲到,AsyncCall任务执行时,会通过调用getResponseWithInterceptorChain方法才能拿到响应结果,这里就涉及到了OkHttp 5大默认拦截器。

RealCall # getResponseWithInterceptorChain

kotlin 复制代码
@Throws(IOException::class)
fun getResponseWithInterceptorChain(): Response {
  // Build a full stack of interceptors.
  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)

  val chain = RealInterceptorChain(interceptors, transmitter, null, 0, originalRequest, this,
      client.connectTimeoutMillis, client.readTimeoutMillis, client.writeTimeoutMillis)

  var calledNoMoreExchanges = false
  try {
    val response = chain.proceed(originalRequest)
    if (transmitter.isCanceled) {
      response.closeQuietly()
      throw IOException("Canceled")
    }
    return response
  } catch (e: IOException) {
    calledNoMoreExchanges = true
    throw transmitter.noMoreExchanges(e) as Throwable
  } finally {
    if (!calledNoMoreExchanges) {
      transmitter.noMoreExchanges(null)
    }
  }
}

在 getResponseWithInterceptorChain方法中,会先创建一个集合存储拦截器,其中会先把我们自定义的拦截器添加进来,然后添加RetryAndFollowUpInterceptorBridgeInterceptorCacheInterceptorConnectInterceptor,如果不是websocket,也会把与网络相关的自定义拦截器添加进来,最后添加了CallServerInterceptor.

然后创建了RealInterceptorChain对象,具体构造函数如下:

kotlin 复制代码
class RealInterceptorChain(
  private val interceptors: List<Interceptor>,
  private val transmitter: Transmitter,
  private val exchange: Exchange?,
  private val index: Int,
  private val request: Request,
  private val call: Call,
  private val connectTimeout: Int,
  private val readTimeout: Int,
  private val writeTimeout: Int
)

这里会把所有的拦截器interceptors,以及拦截器在集合中的位置index、我们创建的请求request当做参数传递,然后调用其proceed方法进行解析。

RealInterceptorChain # proceed

kotlin 复制代码
override fun proceed(request: Request): Response {
  return proceed(request, transmitter, exchange)
}

@Throws(IOException::class)
fun proceed(request: Request, transmitter: Transmitter, exchange: Exchange?): Response {
  if (index >= interceptors.size) throw AssertionError()

  calls++

  // If we already have a stream, confirm that the incoming request will use it.
  check(this.exchange == null || this.exchange.connection()!!.supportsUrl(request.url)) {
    "network interceptor ${interceptors[index - 1]} must retain the same host and port"
  }

  // If we already have a stream, confirm that this is the only call to chain.proceed().
  check(this.exchange == null || calls <= 1) {
    "network interceptor ${interceptors[index - 1]} must call proceed() exactly once"
  }

  // Call the next interceptor in the chain.
  val next = RealInterceptorChain(interceptors, transmitter, exchange,
      index + 1, request, call, connectTimeout, readTimeout, writeTimeout)
  val interceptor = interceptors[index]

  @Suppress("USELESS_ELVIS")
  val response = interceptor.intercept(next) ?: throw NullPointerException(
      "interceptor $interceptor returned null")

  // Confirm that the next interceptor made its required call to chain.proceed().
  check(exchange == null || 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
}

这里其实用到了一个设计模式 - "责任链设计模式",在之前设计模式中,我介绍过责任链设计模式,有兴趣的伙伴可以走直通车看一下:

Android进阶宝典 -- 深究23种设计模式(下)

这里会看到执行proceed方法时,就会先创建一个RealInterceptorChain对象,注意此时index变为index + 1,也就是下一个拦截器的位置,然后根据index取出对应的拦截器并执行intercept方法,同时把下一个RealInterceptorChain对象传递进去。

kotlin 复制代码
class CustomInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {

        return chain.proceed(chain.request())
    }
}

那么此时在当前拦截器执行Interceptor.Chain的proceed方法时,其实已经是下一个拦截器的执行逻辑了,所以如果拦截器需要上下文的关系数据,那么排列顺序上就需要注意了,不能随便排列。

最终经过拦截器的层层处理,拿到了服务端的响应数据,系统的拦截器就不一一介绍了,我们关注原理即可。

1.2 execute方法分析

同步方法相较于异步方法就比较简单了

kotlin 复制代码
override fun execute(): Response {
  synchronized(this) {
    check(!executed) { "Already Executed" }
    executed = true
  }
  transmitter.timeoutEnter()
  transmitter.callStart()
  try {
    client.dispatcher.executed(this)
    return getResponseWithInterceptorChain()
  } finally {
    client.dispatcher.finished(this)
  }
}

因为是同步任务,所以没有并发相关的限制,都是直接放进runningSyncCalls同步队列中,直接执行getResponseWithInterceptorChain方法

Dispatcher # executed

kotlin 复制代码
/** Used by `Call#execute` to signal it is in-flight. */
@Synchronized internal fun executed(call: RealCall) {
  runningSyncCalls.add(call)
}

任务执行完成,直接从同步任务中移除。

kotlin 复制代码
/** Used by `Call#execute` to signal completion. */
internal fun finished(call: RealCall) {
  finished(runningSyncCalls, call)
}

其实OkHttp作为现在主流的网络框架,最大的优势在于其分发器和拦截器,支持高并发,并支持同步异步请求,本文也只是介绍了"两大法宝"的基本原理,后续会对核心的拦截器做详细的分析。

相关推荐
水瓶丫头站住4 小时前
安卓APP如何适配不同的手机分辨率
android·智能手机
xvch4 小时前
Kotlin 2.1.0 入门教程(五)
android·kotlin
幽兰的天空5 小时前
介绍 HTTP 请求如何实现跨域
网络·网络协议·http
lisenustc5 小时前
HTTP post请求工具类
网络·网络协议·http
心平气和️5 小时前
HTTP 配置与应用(不同网段)
网络·网络协议·计算机网络·http
Gworg6 小时前
网站HTTP改成HTTPS
网络协议·http·https
7ACE7 小时前
Wireshark TS | 虚假的 TCP Spurious Retransmission
网络·网络协议·tcp/ip·wireshark·tcpdump
大丈夫立于天地间8 小时前
ISIS基础知识
网络·网络协议·学习·智能路由器·信息与通信
xvch8 小时前
Kotlin 2.1.0 入门教程(七)
android·kotlin
望风的懒蜗牛9 小时前
编译Android平台使用的FFmpeg库
android