三方庫源碼筆記(11)-OkHttp 源碼詳解

                         \\           //
                          \\  .ooo.  //
                           .@@@@@@@@@.
                         :@@@@@@@@@@@@@:
                        :@@. '@@@@@' .@@:
                        @@@@@@@@@@@@@@@@@
                        @@@@@@@@@@@@@@@@@

                   :@@ :@@@@@@@@@@@@@@@@@. @@:
                   @@@ '@@@@@@@@@@@@@@@@@, @@@
                   @@@ '@@@@@@@@@@@@@@@@@, @@@
                   @@@ '@@@@@@@@@@@@@@@@@, @@@
                   @@@ '@@@@@@@@@@@@@@@@@, @@@
                   @@@ '@@@@@@@@@@@@@@@@@, @@@
                   @@@ '@@@@@@@@@@@@@@@@@, @@@
                        @@@@@@@@@@@@@@@@@
                        '@@@@@@@@@@@@@@@'
                           @@@@   @@@@
                           @@@@   @@@@
                           @@@@   @@@@
                           '@@'   '@@'

     :@@@.
   .@@@@@@@:   +@@       `@@      @@`   @@     @@
  .@@@@'@@@@:  +@@       `@@      @@`   @@     @@
  @@@     @@@  +@@       `@@      @@`   @@     @@
 .@@       @@: +@@   @@@ `@@      @@` @@@@@@ @@@@@@  @@;@@@@@
 @@@       @@@ +@@  @@@  `@@      @@` @@@@@@ @@@@@@  @@@@@@@@@
 @@@       @@@ +@@ @@@   `@@@@@@@@@@`   @@     @@    @@@   :@@
 @@@       @@@ +@@@@@    `@@@@@@@@@@`   @@     @@    @@#    @@+
 @@@       @@@ +@@@@@+   `@@      @@`   @@     @@    @@:    @@#
  @@:     .@@` +@@@+@@   `@@      @@`   @@     @@    @@#    @@+
  @@@.   .@@@  +@@  @@@  `@@      @@`   @@     @@    @@@   ,@@
   @@@@@@@@@   +@@   @@@ `@@      @@`   @@@@   @@@@  @@@@#@@@@
    @@@@@@@    +@@   #@@ `@@      @@`   @@@@:  @@@@: @@'@@@@@
                                                     @@:
                                                     @@:
                                                     @@:

前陣子定了個小目標,打算來深入瞭解下幾個常用的開源庫,看下其源碼和實現原理,進行總結並輸出成文章。初定的目標是 EventBus、ARouter、LeakCanary、Retrofit、Glide、OkHttp、Coil 等七個。目前已經完成了十篇關於 EventBus、ARouter、LeakCanary、Retrofit、Glide 的文章,本篇是第十一篇,來對 OkHttp 的源碼進行講解,希望對你有所幫助😎😎

本文基於當前 OkHttp 的最新版本進行講解。值得一提的是,OkHttp 和 OkIO 目前已經被官方用 Kotlin 語言重寫了一遍,所以還沒學 Kotlin 的同學可能連源碼都比較難看懂了,Kotlin 入門可以看我的這篇文章:兩萬六千字帶你 Kotlin 入門

dependencies {
    implementation 'com.squareup.okhttp3:okhttp:4.9.0'
}

先來看一個小例子,後面的講解都會基於這個例子涉及到的模塊來展開

/**
 * 作者:leavesC
 * 時間:2020/11/10 22:11
 * 描述:
 * GitHub:https://github.com/leavesC
 */
const val URL = "https://publicobject.com/helloworld.txt"

fun main() {
    val okHttClient = OkHttpClient.Builder()
        .connectTimeout(Duration.ofSeconds(10))
        .readTimeout(Duration.ofSeconds(10))
        .writeTimeout(Duration.ofSeconds(10))
        .retryOnConnectionFailure(true)
        .build()
    val request = Request.Builder().url(URL).build()
    val call = okHttClient.newCall(request)
    val response = call.execute()
    println(response.body?.string())
}

以上代碼就完成了一次 Get 請求,其包含的操作有:

  1. 通過 Builder 模式得到 OkHttpClient,OkHttpClient 包含了對網絡請求的全局配置信息,包括鏈接超時時間、讀寫超時時間、鏈接失敗重試等各種配置
  2. 通過 Builder 模式得到 Request,Request 包含了本次網絡請求的所有請求參數,包括 url、method、headers、body
  3. 通過 newCall 方法得到 Call,Call 就用於發起請求,可用於執行同步請求(execute)、異步請求(enqueue)、取消請求(cancel)等各種操作
  4. 調用 execute 方法發起同步請求並返回一個 Response 對象,Response 就包含了此次網絡請求的所有返回信息,如果請求失敗的話此方法會拋出異常
  5. 拿到 Response 對象的 body 並以字符串流的方式進行讀取,打印結果即文章開頭的 Android 機器人彩蛋

一、OkHttpClient

OkHttpClient 使用了 Builder 模式來完成初始化,其提供了很多的配置參數,每個選項都有默認值,但大多數時候我們還是需要來進行自定義,所以也有必要來了解下其包含的所有參數

class Builder constructor() {
    //調度器
    internal var dispatcher: Dispatcher = Dispatcher()
    //連接池
    internal var connectionPool: ConnectionPool = ConnectionPool()
    //攔截器列表
    internal val interceptors: MutableList<Interceptor> = mutableListOf()
    //網絡攔截器列表
    internal val networkInterceptors: MutableList<Interceptor> = mutableListOf()
    //事件監聽
    internal var eventListenerFactory: EventListener.Factory = EventListener.NONE.asFactory()
    //連接失敗的時候是否重試
    internal var retryOnConnectionFailure = true 
    //源服務器身份驗證
    internal var authenticator: Authenticator = Authenticator.NONE
    //是否允許重定向
    internal var followRedirects = true
    //是否允許ssl重定向
    internal var followSslRedirects = true
    //Cookie
    internal var cookieJar: CookieJar = CookieJar.NO_COOKIES
    //緩存
    internal var cache: Cache? = null
    //DNS
    internal var dns: Dns = Dns.SYSTEM
    //代理
    internal var proxy: Proxy? = null
    //代理選擇器
    internal var proxySelector: ProxySelector? = null
    //代理身份驗證
    internal var proxyAuthenticator: Authenticator = Authenticator.NONE
    //Socket工廠
    internal var socketFactory: SocketFactory = SocketFactory.getDefault()
    //安全套接層
    internal var sslSocketFactoryOrNull: SSLSocketFactory? = null
    internal var x509TrustManagerOrNull: X509TrustManager? = null
    internal var connectionSpecs: List<ConnectionSpec> = DEFAULT_CONNECTION_SPECS
    //HTTP 協議
    internal var protocols: List<Protocol> = DEFAULT_PROTOCOLS
    //主機名字確認
    internal var hostnameVerifier: HostnameVerifier = OkHostnameVerifier
    //證書鏈
    internal var certificatePinner: CertificatePinner = CertificatePinner.DEFAULT
    internal var certificateChainCleaner: CertificateChainCleaner? = null
    internal var callTimeout = 0
    internal var connectTimeout = 10_000
    //讀超時
    internal var readTimeout = 10_000
    //寫超時
    internal var writeTimeout = 10_000
    //ping 之間的時間間隔
    internal var pingInterval = 0
    internal var minWebSocketMessageToCompress = RealWebSocket.DEFAULT_MINIMUM_DEFLATE_SIZE
    internal var routeDatabase: RouteDatabase? = null
}

二、Request

Request 包含了網絡請求時的所有請求參數,一共包含以下五個:

  1. url。即本次的網絡請求地址以及可能包含的 query 鍵值對
  2. method。即請求方式,可選參數有 GET、HEAD、POST、DELETE、PUT、PATCH
  3. headers。即請求頭,可用來存 token、時間戳等
  4. body。即請求體
  5. tags。可用來唯一標識本次請求
    internal var url: HttpUrl? = null
    internal var method: String
    internal var headers: Headers.Builder
    internal var body: RequestBody? = null
    /** A mutable map of tags, or an immutable empty map if we don't have any. */
    internal var tags: MutableMap<Class<*>, Any> = mutableMapOf()

三、Call

當調用 okHttClient.newCall(request)時就會得到一個 Call 對象

  /** Prepares the [request] to be executed at some point in the future. */
  override fun newCall(request: Request): Call = RealCall(this, request, forWebSocket = false)

Call 是一個接口,我們可以將其看做是網絡請求的啓動器,可用於發起同步請求或異步請求,但重複發起多次請求的話會拋出異常

interface Call : Cloneable {
    
  //返回本次網絡請求的 Request 對象
  fun request(): Request
    
  //發起同步請求,可能會拋出異常
  @Throws(IOException::class)
  fun execute(): Response
    
  //發起異步請求,通過 Callback 來回調最終結果 
  fun enqueue(responseCallback: Callback)

  //取消網絡請求
  fun cancel()

  //是否已經發起過請求
  fun isExecuted(): Boolean

  //是否已經取消請求
  fun isCanceled(): Boolean
  
  //超時計算
  fun timeout(): Timeout

  //同個 Call 不允許重複發起請求,想要再次發起請求可以通過此方法得到一個新的 Call 對象
  public override fun clone(): Call

  fun interface Factory {
    fun newCall(request: Request): Call
  }
}

newCall 方法返回的實際類型是 RealCall,它是 Call 接口的唯一實現類

當我們調用 execute 方法發起同步請求時,其主要邏輯是:

  1. 判讀是否重複請求
  2. 事件記錄
  3. 將自身加入到 dispatcher 中,並在請求結束時從 dispatcher 中移除自身
  4. 通過 getResponseWithInterceptorChain 方法得到 Response 對象
class RealCall(
  val client: OkHttpClient,
  /** The application's original request unadulterated by redirects or auth headers. */
  val originalRequest: Request,
  val forWebSocket: Boolean
) : Call {
    
  override fun execute(): Response {
    check(executed.compareAndSet(false, true)) { "Already Executed" }
    timeout.enter()
    callStart()
    try {
      client.dispatcher.executed(this)
      return getResponseWithInterceptorChain()
    } finally {
      client.dispatcher.finished(this)
    }
  }
  
}

四、Dispatcher

從上面的分析可以看出來,getResponseWithInterceptorChain 方法就是重頭戲了,其返回了我們最終得到的 Response。但這裏先不介紹該方法,先來看看 Dispatcher 的邏輯

Dispatcher 是一個調度器,用於對全局的網絡請求進行緩存調度,其包含以下幾個成員變量

  var maxRequests = 64

  var maxRequestsPerHost = 5

  /** 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>()

  • maxRequests。同一時間允許併發執行網絡請求的最大線程數
  • maxRequestsPerHost。同一 host 下的最大同時請求數
  • readyAsyncCalls。保存當前等待執行的異步任務
  • runningAsyncCalls。保存當前正在執行的異步任務
  • runningSyncCalls。保存當前正在執行的同步任務

客戶端不應該無限制地同時發起多個網絡請求,因爲除了網絡資源所限外,系統資源也是有限的,每個請求都需要由一個線程來執行,而系統支持併發執行的線程數量是有限的,所以 OkHttp 內部就使用 maxRequests 來控制同時執行異步請求的最大線程數。此外,OkHttp 爲了提高效率,允許多個指向同一 host 的網絡請求共享同一個 Socket,而最大共享數量即 maxRequestsPerHost

爲了統計以上兩個運行參數,就需要使用 readyAsyncCalls、runningAsyncCalls 和 runningSyncCalls 來保存當前正在執行或者準備執行的網絡請求。runningSyncCalls 用於保存當前正在執行的同步任務,其存儲的是 RealCall。readyAsyncCalls 和 runningAsyncCalls 用於保存異步任務,其存儲的是 AsyncCall

1、同步請求

RealCall 的 execute() 方法在開始請求前,會先將自身傳給 dispatcher,在請求結束後又會從 dispatcher 中移除

class RealCall(
  val client: OkHttpClient,
  /** The application's original request unadulterated by redirects or auth headers. */
  val originalRequest: Request,
  val forWebSocket: Boolean
) : Call {
 
  override fun execute(): Response {
    check(executed.compareAndSet(false, true)) { "Already Executed" }
    timeout.enter()
    callStart()
    try {
        //添加到 dispatcher
      client.dispatcher.executed(this)
      return getResponseWithInterceptorChain()
    } finally {
        //從 dispatcher 中移除
      client.dispatcher.finished(this)
    }
  }
    
}

Dispatcher 內部也只是相應的將 RealCall 添加到 runningSyncCalls 中或者是將其從 runningSyncCalls 中移除,保存到 runningSyncCalls 的目的是爲了方便統計當前所有正在運行的請求總數以及能夠取消所有請求。此外,由於同步請求會直接運行在調用者所在線程上,所以同步請求並不會受 maxRequests 的限制

class Dispatcher constructor() {
    
      /** Used by [Call.execute] to signal it is in-flight. */
      @Synchronized 
      internal fun executed(call: RealCall) {
        runningSyncCalls.add(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()
        }
     }
    
}

2、異步請求

RealCall 的 enqueue方法會將外部傳入的 Callback 包裝爲一個 AsyncCall 對象後傳給 dispatcher

class RealCall(
  val client: OkHttpClient,
  /** The application's original request unadulterated by redirects or auth headers. */
  val originalRequest: Request,
  val forWebSocket: Boolean
) : Call {
    
  override fun enqueue(responseCallback: Callback) {
    check(executed.compareAndSet(false, true)) { "Already Executed" }
    callStart()
    client.dispatcher.enqueue(AsyncCall(responseCallback))
  }
    
}

由於 enqueue對應的是異步請求,所以 OkHttp 內部就需要自己構造一個線程來執行請求,在請求結束後再通過 Callback 來將結果值回調給外部,異步請求邏輯對應的載體就是 AsyncCall 這個類

AsyncCall 是 RealCall 的非靜態內部類,所以 AsyncCall 可以訪問到 RealCall 的所有變量和方法。此外,AsyncCall 繼承了 Runnable 接口,其 executeOn 方法就用於傳入一個線程池對象來執行run 方法。run 方法內還是調用了 getResponseWithInterceptorChain()方法來獲取 response,並通過 Callback 來將執行結果(不管成功還是失敗)回調出去,在請求結束後也會將自身從 dispatcher 中移除

  internal inner class AsyncCall(
    private val responseCallback: Callback
  ) : Runnable {
    @Volatile var callsPerHost = AtomicInteger(0)
      private set

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

    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!
        }
      }
    }

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

Dispatcher 在拿到 AsyncCall 對象後,會先將其存到 readyAsyncCalls 中,然後通過 findExistingCallWithHost方法來查找當前是否有指向同一 Host 的異步請求,有的話則交換 callsPerHost 變量,該變量就用於標記當前指向同一 Host 的請求數量,最後調用 promoteAndExecute 方法來判斷當前是否允許發起請求

class Dispatcher constructor() {
 
   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) {
        //查找當前是否有指向同一 Host 的異步請求
        val existingCall = findExistingCallWithHost(call.host)
        if (existingCall != null) call.reuseCallsPerHostFrom(existingCall)
      }
    }
    promoteAndExecute()
  }

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

由於當前正在執行的網絡請求總數可能已經達到限制,或者是指向同一 Host 的請求也達到限制了,所以 promoteAndExecute()方法就用於從待執行列表 readyAsyncCalls 中獲取當前符合運行條件的所有請求,將請求存到 runningAsyncCalls 中,並調用線程池來執行

private fun promoteAndExecute(): Boolean {
    this.assertThreadDoesntHoldLock()

    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.
        //如果指向同個 Host 的請求總數已經超出限制,則取下一個請求
        if (asyncCall.callsPerHost.get() >= this.maxRequestsPerHost) continue // Host max capacity.

        i.remove()
        //將 callsPerHost 遞增加一,表示指向該 Host 的鏈接數加一了
        asyncCall.callsPerHost.incrementAndGet()
        //將 asyncCall 存到可執行列表中
        executableCalls.add(asyncCall)
        //將 asyncCall 存到正在執行列表中
        runningAsyncCalls.add(asyncCall)
      }
      isRunning = runningCallsCount() > 0
    }

    //執行所有符合條件的請求
    for (i in 0 until executableCalls.size) {
      val asyncCall = executableCalls[i]
      asyncCall.executeOn(executorService)
    }

    return isRunning
  }

3、ArrayDeque

上面有講到,三種請求的存儲容器是 ArrayDeque。ArrayDeque 屬於非線程安全的雙端隊列,所以涉及到多線程操作時都需要外部主動線程同步。那麼讓我們想一想,OkHttp 選擇 ArrayDeque 作爲任務容器的理由是什麼?以我粗淺的眼光來看,有以下幾點:

  • ArrayDeque 內部使用數組結構來存儲數據,元素具有明確的先後順序,這符合我們對網絡請求先到先執行的基本預期
  • 在選擇符合運行條件的異步請求時,需要對 readyAsyncCalls 進行遍歷,數組在遍歷效率上會比較高
  • 在遍歷到符合條件的請求後,需要將請求從 readyAsyncCalls 中移除並轉移到 runningAsyncCalls 中,而 ArrayDeque 作爲雙端隊列,在內存空間利用率上比較高
  • Dispatcher 面對的就是多線程環境,本身就需要進行線程同步,選擇 ArrayDeque 這個非線程安全的容器可以省去多餘的線程同步消耗

4、線程池

OkHttp 的異步請求是交由其內部的線程池來完成的,該線程池就長這樣:

  private var executorServiceOrNull: ExecutorService? = null

  @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!!
    }

該線程池的參數設置有什麼優勢呢?以我粗淺的眼光來看,有以下兩點:

  1. 核心線程數爲 0,線程超時時間是 60 秒。說明在沒有待執行的任務的時候,如果線程閒置了 60 秒,那麼線程就會被回收,這可以避免空閒線程白白浪費系統資源,適合於移動設備資源緊缺的情景
  2. 允許的最大線程數爲 Int.MAX_VALUE,可以看做是完全沒有限制的,且任務隊列是 SynchronousQueue。SynchronousQueue 的特點是當有任務入隊時,必須等待該任務被消費否則入隊操作就會一直被阻塞,而由於線程池允許的最大線程數量是無限的,所以每個入隊的任務都能馬上交由線程處理(交付給空閒線程或者新建一個線程來處理),這就保證了任務的處理及時性,符合我們對網絡請求應該儘快發起並完成的期望

雖然線程池本身對於最大線程數幾乎沒有限制,但是由於提交任務的操作還受 maxRequests 的控制,所以實際上該線程池最多同時運行 maxRequests 個線程

5、推動請求執行

既然 OkHttp 內部的線程池是不可能無限制地新建線程來執行請求的,那麼當請求總數已達到 maxRequests 後,後續的請求只能是先處於等待狀態,那麼這些等待狀態的請求會在什麼時候被啓動呢?

同步請求和異步請求結束後都會調用到 Dispatcher 的兩個 finished 方法,在這兩個方法裏又會觸發到 promoteAndExecute()方法去遍歷任務列表來執行,此時就推動了待處理列表的任務執行操作。所以說,Dispatcher 中的請求都可以看做是在自發性地啓動,每個請求結束都會自動觸發下一個請求執行(如果有的話),省去了多餘的定時檢查這類操作

  /** Used by [AsyncCall.run] to signal completion. */
  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()
    }
  }

6、總結

  • 如果是同步請求,那麼網絡請求過程就會直接在調用者所在線程上完成,不受 Dispatcher 的控制
  • 如果是異步請求,該請求會先存到待執行列表 readyAsyncCalls 中,該請求是否可以立即發起受 maxRequests 和 maxRequestsPerHost 兩個條件的限制。如果符合條件,那麼就會從 readyAsyncCalls 取出並存到 runningAsyncCalls 中,然後交由 OkHttp 內部的線程池來執行
  • 不管外部是同步請求還是異步請求,內部都是通過調用getResponseWithInterceptorChain()方法來拿到 Response 的
  • Dispatcher 內部的線程池本身允許同時運行 Int.MAX_VALUE 個線程,但是實際上的線程數量還是受 maxRequests 的控制

五、RealInterceptorChain

重點再來看 getResponseWithInterceptorChain()方法,其主要邏輯就是通過攔截器來完成整個網絡請求過程。在該方法中,除了會獲取外部主動設置的攔截器外,也會默認添加以下幾個攔截器

  1. RetryAndFollowUpInterceptor。負責失敗重試以及重定向
  2. BridgeInterceptor。負責對用戶構造的 Request 進行轉換,添加必要的 header 和 cookie,在得到 response 後如果有需要的會進行 gzip 解壓
  3. CacheInterceptor。用於處理緩存
  4. ConnectInterceptor。負責和服務器建立連接
  5. CallServerInterceptor。負責向服務器發送請求和從服務器接收數據

最後,request 和 interceptors 會用來生成一個 RealInterceptorChain 對象,由其來最終返回 response

  @Throws(IOException::class)
  internal 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) {
      //如果不是 WebSocket 的話,那就再添加開發者設置的 NetworkInterceptors
      interceptors += client.networkInterceptors
    }
      
    //CallServerInterceptor 是實際上發起網絡請求的地方
    interceptors += CallServerInterceptor(forWebSocket)

    val chain = RealInterceptorChain(
        call = this,
        interceptors = interceptors,
        index = 0,
        exchange = null,
        request = originalRequest,
        connectTimeoutMillis = client.connectTimeoutMillis,
        readTimeoutMillis = client.readTimeoutMillis,
        writeTimeoutMillis = client.writeTimeoutMillis
    )

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

Interceptor 是 OkHttp 裏很重要的一環,OkHttp 也是靠此爲開發者提供了很高的自由度。Interceptor 接口本身只包含一個 intercept 方法,在此方法內可拿到原始的 Request 對象以及最終的 Response

fun interface Interceptor {
   @Throws(IOException::class)
   fun intercept(chain: Chain): Response   
}

例如,我們可以自定義一個 LogInterceptor 來打印網絡請求的請求參數以及最終的返回值

class LogInterceptor : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        println(request)
        val response = chain.proceed(request)
        println(response)
        return response
    }

}

Interceptor 的實現初衷是爲了給開發者提供一個可以控制網絡請求的發起過程收尾工作的入口,例如**添加 header、日誌記錄、請求攔截、ResponseBody修改 **等,每個 Interceptor 只負責自己關心的操作,那麼勢必就會有添加多個 Interceptor 的需求

我們知道,只有讓每個 Interceptor 都依次處理完 request 之後,OkHttp 才能根據最終的 request 對象去聯網請求得到 response,所以每個 Interceptor 需要依次拿到 request 進行自定義處理。請求到 response 後,Interceptor 可能還需要對 response 進行處理,那麼就還需要將 response 再依次傳遞給每個 Interceptor。那麼,怎麼實現將多個 Interceptor 給串聯起來呢?

這裏來看一個簡化版本的 Interceptor 實現思路

假設我們自己定義的 Interceptor 實現類有兩個:LogInterceptor 和 HeaderInterceptor,這裏只是簡單地將獲取到 request 和 response 的時機給打印出來,重點是要看每個 Interceptor 的先後調用順序。爲了將兩個 Interceptor 給串聯起來,RealInterceptorChain 會循環獲取 index 指向的下一個 Interceptor 對象,每次構建一個新的 RealInterceptorChain 對象作爲參數來調用 intercept 方法,這樣外部只需要調用一次 realInterceptorChain.proceed 就可以拿到最終的 response 對象

/**
 * 作者:leavesC
 * 時間:2020/11/11 16:08
 * 描述:
 */
class Request

class Response

interface Chain {

    fun request(): Request

    fun proceed(request: Request): Response

}

interface Interceptor {

    fun intercept(chain: Chain): Response

}

class RealInterceptorChain(
    private val request: Request,
    private val interceptors: List<Interceptor>,
    private val index: Int
) : Chain {

    private fun copy(index: Int): RealInterceptorChain {
        return RealInterceptorChain(request, interceptors, index)
    }

    override fun request(): Request {
        return request
    }

    override fun proceed(request: Request): Response {
        val next = copy(index = index + 1)
        val interceptor = interceptors[index]
        val response = interceptor.intercept(next)
        return response
    }

}

class LogInterceptor : Interceptor {
    override fun intercept(chain: Chain): Response {
        val request = chain.request()
        println("LogInterceptor -- getRequest")
        val response = chain.proceed(request)
        println("LogInterceptor ---- getResponse")
        return response
    }
}

class HeaderInterceptor : Interceptor {
    override fun intercept(chain: Chain): Response {
        val request = chain.request()
        println("HeaderInterceptor -- getRequest")
        val response = chain.proceed(request)
        println("HeaderInterceptor ---- getResponse")
        return response
    }
}

fun main() {
    val interceptorList = mutableListOf<Interceptor>()
    interceptorList.add(LogInterceptor())
    interceptorList.add(HeaderInterceptor())
    val request = Request()
    val realInterceptorChain = RealInterceptorChain(request, interceptorList, 0)
    val response = realInterceptorChain.proceed(request)
    println("main response")
}

上面的代碼看着思路還可以,可是當運行後就會發現拋出了 IndexOutOfBoundsException,因爲代碼裏並沒有對 index 進行越界判斷。而且,上面的代碼也缺少了一個真正的生成 Response 對象的地方,每個 Interceptor 只是在進行中轉調用而已,因此還需要一個來真正地完成網絡請求並返回 Response 對象的地方,即 CallServerInterceptor

所以,CallServerInterceptor 的intercept 方法就用來真正地執行網絡請求並生成 Response 對象,在這裏就不能再調用 proceed 方法了,且 CallServerInterceptor 需要放在 interceptorList 的最後一位

class CallServerInterceptor : Interceptor {
    override fun intercept(chain: Chain): Response {
        val request = chain.request()
        println("CallServerInterceptor -- getRequest")
        val response = Response()
        println("CallServerInterceptor ---- getResponse")
        return response
    }
}

fun main() {
    val interceptorList = mutableListOf<Interceptor>()
    interceptorList.add(LogInterceptor())
    interceptorList.add(HeaderInterceptor())
    interceptorList.add(CallServerInterceptor())
    val request = Request()
    val realInterceptorChain = RealInterceptorChain(request, interceptorList, 0)
    val response = realInterceptorChain.proceed(request)
    println("main response")
}

最終的運行結果如下所示,可以看出來,intercept 方法是根據添加順序來調用,而 response 是按照反方向來傳遞

LogInterceptor -- getRequest
HeaderInterceptor -- getRequest
CallServerInterceptor -- getRequest
CallServerInterceptor ---- getResponse
HeaderInterceptor ---- getResponse
LogInterceptor ---- getResponse
main response

以上代碼我簡化了 OkHttp 在實現 RealInterceptorChain 時的思路,其本質上就是通過將多個攔截器以責任鏈的方式來一層層調用,上一個攔截器處理完後將就將結果傳給下一個攔截器,直到最後一個攔截器(即 CallServerInterceptor )處理完後將 Response 再一層層往上傳遞

class RealInterceptorChain(
  internal val call: RealCall,
  private val interceptors: List<Interceptor>,
  private val index: Int,
  internal val exchange: Exchange?,
  internal val request: Request,
  internal val connectTimeoutMillis: Int,
  internal val readTimeoutMillis: Int,
  internal val writeTimeoutMillis: Int
) : Interceptor.Chain {
    
  internal fun copy(
    index: Int = this.index,
    exchange: Exchange? = this.exchange,
    request: Request = this.request,
    connectTimeoutMillis: Int = this.connectTimeoutMillis,
    readTimeoutMillis: Int = this.readTimeoutMillis,
    writeTimeoutMillis: Int = this.writeTimeoutMillis
  ) = RealInterceptorChain(call, interceptors, index, exchange, request, connectTimeoutMillis,
      readTimeoutMillis, writeTimeoutMillis)
    
  @Throws(IOException::class)
  override fun proceed(request: Request): Response {
    ···
    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")
    ···
    return response
  }
    
}

六、Interceptor

我們在構建 OkHttpClient 的時候,添加攔截器的方法分爲兩類:addInterceptoraddNetworkInterceptor

    val okHttClient = OkHttpClient.Builder()
        .addInterceptor { chain ->
            chain.proceed(chain.request())
        }
        .addNetworkInterceptor { chain ->
            chain.proceed(chain.request())
        }
        .build()

Interceptor 和 NetworkInterceptor 分別被稱爲應用攔截器網絡攔截器,那麼它們有什麼區別呢?

前面有講到,OkHttp 在執行攔截器的時候,是按照如下順序的,這個順序就已經決定了不同攔截器的調用時機差異

    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)

  • 由於應用攔截器處於列表頭部,所以在整個責任鏈路中應用攔截器會首先被執行,即使之後在 RetryAndFollowUpInterceptor 中發生了請求失敗重試或者網絡重定向等情況,應用攔截器也只會被觸發一次,但網絡攔截器會被調用多次
  • 網絡攔截器位於 CacheInterceptor 之後,那麼當 CacheInterceptor 命中緩存的時候就不會去執行網絡請求了,此時網絡攔截器就不會被調用,因此網絡攔截器是存在短路的可能。此外,網絡攔截器位於 ConnectInterceptor 之後,在調用網絡攔截器之前就已經準備好網絡鏈接了,說明網絡攔截器本身就關聯着實際的網絡請求邏輯
  • 從單次請求流程上來看,應用攔截器被調用並不意味着真正有發起了網絡請求,而網絡攔截器被調用就說明的確發起了一次網絡請求。因此如果我們希望通過攔截器來記錄網絡請求詳情的話,就需要考慮兩者的調用時機差異:應用攔截器無法感知到 OkHttp 自動添加的一些 header,但是網絡攔截器可以;應用攔截器除非主動中斷請求,否則每次請求一定都會被執行,但網絡攔截器可能存在被短路的可能

借用官方的一張圖片來表示

這裏可以根據 square 官方提供的一個例子,來實現在下載一張 10 MB 圖片的時候通過攔截器對下載進度進行監聽,並同時把圖片下載到系統的桌面

實現思路就是對原始的 ResponseBody 進行多一層代理,計算已經從網絡中讀取到的字節數和資源的 contentLength 之間的百分比,從而得到下載進度。此外,因爲該攔截器是和確切的網絡請求相關,所以應該要設爲網絡攔截器才比較合理

/**
 * 作者:leavesC
 * 時間:2020/11/14 15:49
 * 描述:
 * GitHub:https://github.com/leavesC
 */
fun main() {
    run()
}

interface ProgressListener {
    fun update(bytesRead: Long, contentLength: Long, done: Boolean)
}

private fun run() {
    val request = Request.Builder()
        .url("https://images.pexels.com/photos/5177790/pexels-photo-5177790.jpeg")
        .build()
    val progressListener: ProgressListener = object : ProgressListener {
        var firstUpdate = true
        override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
            if (done) {
                println("completed")
            } else {
                if (firstUpdate) {
                    firstUpdate = false
                    if (contentLength == -1L) {
                        println("content-length: unknown")
                    } else {
                        System.out.format("content-length: %d\n", contentLength)
                    }
                }
                println(bytesRead)
                if (contentLength != -1L) {
                    System.out.format("%d%% done\n", 100 * bytesRead / contentLength)
                }
            }
        }
    }
    val client = OkHttpClient.Builder()
        .addNetworkInterceptor { chain: Interceptor.Chain ->
            val originalResponse = chain.proceed(chain.request())
            originalResponse.newBuilder()
                .body(ProgressResponseBody(originalResponse.body!!, progressListener))
                .build()
        }
        .build()
    client.newCall(request).execute().use { response ->
        if (!response.isSuccessful) {
            throw IOException("Unexpected code $response")
        }
        val desktopDir = FileSystemView.getFileSystemView().homeDirectory
        val imageFile = File(desktopDir, "${System.currentTimeMillis()}.jpeg")
        imageFile.createNewFile()
        //讀取 InputStream 寫入到圖片文件中
        response.body!!.byteStream().copyTo(imageFile.outputStream())
    }
}

private class ProgressResponseBody constructor(
    private val responseBody: ResponseBody,
    private val progressListener: ProgressListener
) : ResponseBody() {

    private var bufferedSource: BufferedSource? = null

    override fun contentType(): MediaType? {
        return responseBody.contentType()
    }

    override fun contentLength(): Long {
        return responseBody.contentLength()
    }

    override fun source(): BufferedSource {
        if (bufferedSource == null) {
            bufferedSource = source(responseBody.source()).buffer()
        }
        return bufferedSource!!
    }

    private fun source(source: Source): Source {

        return object : ForwardingSource(source) {

            var totalBytesRead = 0L

            @Throws(IOException::class)
            override fun read(sink: Buffer, byteCount: Long): Long {
                val bytesRead = super.read(sink, byteCount)
                // read() returns the number of bytes read, or -1 if this source is exhausted.
                totalBytesRead += if (bytesRead != -1L) bytesRead else 0
                progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
                return bytesRead
            }
        }
    }

}

進度輸出就類似於:

content-length: 11448857
467
0% done
1836
0% done
3205

···

99% done
11442570
99% done
11448857
100% done
completed

七、結尾

關於 OkHttp 的源碼講解到這裏就結束了,但本文還缺少了對 ConnectInterceptor 和 CallServerInterceptor 的講解,這兩者是 OkHttp 完成實際網絡請求的地方,涉及到了 Connection 和 Socket 這些比較底層的領域,我沒法講得多清晰,就直接略過這塊內容了~~

OkHttp 的運行效率很高,但在使用上還是比較原始,一般我們還是需要在 OkHttp 之上進行一層封裝,Retrofit 就是一個對 OkHttp 的優秀封裝庫,對 Retrofit 的源碼講解可以看我的這篇文章:三方庫源碼筆記(7)-超詳細的 Retrofit 源碼解析

下篇文章就來寫關於 OkHttp 攔截器的實戰內容吧

一個人走得快,一羣人走得遠,寫了文章就只有自己看那得有多孤單,只希望對你有所幫助😂😂😂

查看更多文章請點擊關注:字節數組

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章