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

相关推荐
CYRUS STUDIO5 分钟前
ARM64汇编寻址、汇编指令、指令编码方式
android·汇编·arm开发·arm·arm64
weixin_449310841 小时前
高效集成:聚水潭采购数据同步到MySQL
android·数据库·mysql
Zender Han1 小时前
Flutter自定义矩形进度条实现详解
android·flutter·ios
yfs10241 小时前
压缩Minio桶中的文件为ZIP,并通过 HTTP 响应输出
网络·网络协议·http
超栈2 小时前
HCIP(11)-期中综合实验(BGP、Peer、OSPF、VLAN、IP、Route-Policy)
运维·网络·网络协议·计算机网络·web安全·网络安全·信息与通信
დ旧言~2 小时前
【网络】应用层——HTTP协议
开发语言·网络·网络协议·http·php
白乐天_n3 小时前
adb:Android调试桥
android·adb
姑苏风7 小时前
《Kotlin实战》-附录
android·开发语言·kotlin
数据猎手小k10 小时前
AndroidLab:一个系统化的Android代理框架,包含操作环境和可复现的基准测试,支持大型语言模型和多模态模型。
android·人工智能·机器学习·语言模型
你的小1011 小时前
JavaWeb项目-----博客系统
android