OkHttp 源碼解析

使用流程

我們都知道一個簡單的OkHttp請求流程是這么寫的:

        val url = "http://www.baidu.com/"
        //1.新建OKHttpClient客戶端
        val okHttpClient = OkHttpClient()
        //新建一個Request對象
        val request = Request.Builder()
            .url(url)
            .build()
        //2.Response為OKHttp中的響應
        okHttpClient.newCall(request).enqueue(object :Callback{
            override fun onFailure(call: Call, e: IOException) {}
            override fun onResponse(call: Call, response: Response) {}
        })

在使用OkHttp發(fā)起一次請求時,對于使用者最少存在OkHttpClient、Request與Call三個角色逢勾,其中OkHttpClient和Request的創(chuàng)建可以通過它提供的Builder(建造者模式)。而Call則是把Request交給OkHttpClient之后返回的一個已準備好執(zhí)行的請求。

但是它的實際請求流程是這樣的:


請求流程

發(fā)現(xiàn)調(diào)用execute或者enqueue之后會去到dispatcher(分發(fā)器)再到interceptors(攔截器),這兩個是什么呢纱烘?后面會講到

先來看看execute或者enqueue調(diào)用后做了什么事。點擊execute或者enqueue發(fā)現(xiàn)是接口方法,那就找它的實現(xiàn)體山宾,看看newCall()

  //OkHttpClient.kt
  override fun newCall(request: Request): Call {
    return RealCall.newRealCall(this, request, forWebSocket = false)
  }

再點RealCall.newRealCall(),發(fā)現(xiàn)當OkHttpClient調(diào)用newCall時其實調(diào)用的是RealCall.newRealCall()翎猛,所以具體實現(xiàn)都在RealCall里面。

假如調(diào)用異步請求enqueue是這樣的:okHttpClient.newCall(request).enqueue() -> RealCall.newRealCall().enqueue()

  //RealCall.kt
  override fun enqueue(responseCallback: Callback) {
    //1
    synchronized(this) {
      check(!executed) { "Already Executed" }
      executed = true
    }
    //2
    transmitter.callStart()
    //3
    client.dispatcher.enqueue(AsyncCall(responseCallback))
  }

分析:

  • 1:首先用判斷這個Call是否被調(diào)用,如果沒有則executed = true遗座,繼續(xù)往下走;如果被調(diào)用了則會拋異常。
  • 2:用于監(jiān)聽請求的信息宽堆,包括DNS的解析開始和結束時間,連接的開始和結束時間。
  • 3:重點在這里,調(diào)用了OkHttpClient的dispatcher的enqueue方法,先不看AsyncCall是什么驾茴,其實到這里就是:OkHttpClient.newCall().enqueue() -> RealCall.newCall().enqueue() -> Dispatcher.enqueue()

而Dispatcher是一個分發(fā)器,至于為什么被叫分發(fā)器击碗,看完下面的解析你就知道了。

分發(fā)器Dispatcher

在Dispatcher中這么兩個變量、三個雙向隊列和一個線程池:

  //Dispatcher.kt
  //異步請求同時存在的最大請求
  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>()
  //SynchronousQueue的線程池
  @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!!
    }

第一個隊列是在異步請求中,用于存儲等待執(zhí)行Call的隊列猖吴;第二個是在異步請求中绑谣,存儲正在執(zhí)行Call的隊列幌衣;第三個則是同步請求中,存儲正在執(zhí)行Call的隊列楚里。至于在異步請求中,當前Call是去到等待隊列還是執(zhí)行隊列,取決于maxRequests和maxRequestsPerHost。

maxRequests 是當前最多同時的請求個數(shù)满葛,maxRequestsPerHost 是當前主機最多的請求個數(shù)。

executorService線程池則用SynchronousQueue存儲,主要是避免任務假如成功,導致任務等待阻塞,所以SynchronousQueue讓其加入隊列失敿ぢ省(SynchronousQueue是沒有容量的隊列)低缩,直接新建新的線程執(zhí)行該任務

剛才說到最終會來到Dispatcher.enqueue()

   //Dispatcher.kt
  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()
  }

首先會去把這個Call加入到等待隊列中讳推,然后直接走promoteAndExecute()

promoteAndExecute()的作用是處理對等待隊列中的Call,簡單點來說就是等待隊列為空,則不作處理镰吆;若不為空摧找,根據(jù)情況分析:

  //Dispatcher.kt
  private fun promoteAndExecute(): Boolean {
    this.assertThreadDoesntHoldLock()

    val executableCalls = mutableListOf<AsyncCall>()
    val isRunning: Boolean

    synchronized(this) {
      //1
      val i = readyAsyncCalls.iterator()
      while (i.hasNext()) {
        val asyncCall = i.next()
        //2
        if (runningAsyncCalls.size >= this.maxRequests) break // Max capacity.
        if (asyncCall.callsPerHost().get() >= this.maxRequestsPerHost) continue // Host max capacity.

        i.remove()
        asyncCall.callsPerHost().incrementAndGet()
        //3
        executableCalls.add(asyncCall)
        runningAsyncCalls.add(asyncCall)
      }
      isRunning = runningCallsCount() > 0
    }
    //4
    for (i in 0 until executableCalls.size) {
      val asyncCall = executableCalls[i]
      asyncCall.executeOn(executorService)
    }

    return isRunning
  }

分析:
在這之前,注意enqueue的時候先把這個Call加入等待隊列了

  • 1:首先把等待隊列遍歷,往下遍歷堡牡,并取出當前的Call妖胀。
  • 2:首先判斷當前運行中的隊列大小是不是等于或者超過設定的maxRequests的值爬坑,如果是就break出;如果不會則往下走,判斷當前訪問的主機請求數(shù)等于或者超過設定的當前主機請求數(shù),則跳過繼續(xù)往下下一個Call判斷刚夺;如果不會則往下走箩做,把這個Call從等待隊列中移除(隊列只是用來存儲Call安吁,表示當前的Call的狀態(tài)),并把當前Call訪問的主機數(shù)加一滥玷。
  • 3:把可執(zhí)行的Call加入executableCalls拉盾,并加入運行中的隊列倒得。
  • 4:假如executableCalls為空這里不走讹躯;不為空則遍歷所有可執(zhí)行的Call骗灶,并把線程池executorService丟給executeOn()萝究。

而這個executeOn是AsyncCall里面的绕娘,那就來看看它是什么東西吧,點進去發(fā)現(xiàn)是RealCall的內(nèi)部類,繼承了Runnable,所以是任務

    //AsynCall.class
    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)
        transmitter.noMoreExchanges(ioException)
        responseCallback.onFailure(this@RealCall, ioException)
      } finally {
        if (!success) {
          client.dispatcher.finished(this) // This call is no longer running!
        }
      }
    }

原來把這個線程池丟給它是為了執(zhí)行它的任務,這個任務是它本身,也就是它的run方法

    //AsyncCall.class
    override fun run() {
      threadName("OkHttp ${redactedUrl()}") {
        var signalledCallback = false
        transmitter.timeoutEnter()
        try {
          //1
          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()}", 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 {
          //2
          client.dispatcher.finished(this)
        }
      }
    }

分析:

  • 1:getResponseWithInterceptorChain后面會講到获黔,是OkHttp重要的一部分,作用是攔截器處理請求后渗蟹,得到成功的響應結果诗宣,并把結果回調(diào)出去我們使用的地方,也就是我們代碼中的onResponse忘古;假如異常,則把失敗信息回調(diào)出去,對應代碼中的onFailure。
  • 2:不管成功失敗,都會走finished。

看看Dispatcher.finished是怎么處理的

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

先把當前訪問主機記錄的個數(shù)減一再走finished(runningAsyncCalls, call)

  //Dispatcher.kt
  private fun <T> finished(calls: Deque<T>, call: T) {
    val idleCallback: Runnable?
    synchronized(this) {
      //1
      if (!calls.remove(call)) throw AssertionError("Call wasn't in-flight!")
      idleCallback = this.idleCallback
    }
    //2
    val isRunning = promoteAndExecute()

    if (!isRunning && idleCallback != null) {
      idleCallback.run()
    }
  }
  • 1:在運行隊列中把該Call移除
  • 2:又走了一遍promoteAndExecute()驹碍,這里主要是對等待隊列中的Call處理,因為一個運行中的Call處理完柏蘑,就有空出來的位置給等待隊列中的Call執(zhí)行幸冻,所以在異步請求中,每當處理完一個運行中的Call都會去等待隊列里面去取Call洽损,取到則處理,取不到則不處理革半。

所以分發(fā)器異步請求處理流程大概是這個樣子的:

異步請求流程

小結一下:

  • 如何決定將請求放進ready還是running碑定?
    如果當前正在請求數(shù)為64,則將請求放進ready等待隊列又官,如果小于64延刘,但是已經(jīng)存在同一域名主機的請求5個,也會放進ready六敬;否則放進running隊列立即執(zhí)行碘赖。
  • 從ready移到running的條件是什么?
    每個請求執(zhí)行完成會從running移除外构,同時進行第一步相同邏輯判斷普泡,決定是否移動。
  • 分發(fā)器線程池的作用是什么审编?
    無等待撼班,高并發(fā)。

異步請求流程講完了垒酬,看看同步請求

  //RealCall.kt
  override fun execute(): Response {
    synchronized(this) {
      check(!executed) { "Already Executed" }
      executed = true
    }
    transmitter.timeoutEnter()
    transmitter.callStart()
    try {
      //1
      client.dispatcher.executed(this)
      return getResponseWithInterceptorChain()
    } finally {
      //2
      client.dispatcher.finished(this)
    }
  }

  //Dispatcher.kt
  @Synchronized internal fun executed(call: RealCall) {
    runningSyncCalls.add(call)
  }
  • 1:同步請求就比較簡單了砰嘁,先把Call加入運行中的隊列件炉,因為同步隊列所以是在當前線程執(zhí)行而不需要線程池,然后getResponseWithInterceptorChain得到響應結果返回出來矮湘。
  • 2:最后會走Dispatcher.finished斟冕,這個方法個異步的finished參數(shù)不一樣,一個是AsyncCall板祝,一個是RealCall宫静。
  //Dispatcher.kt
  internal fun finished(call: RealCall) {
    finished(runningSyncCalls, call)
  }

最后調(diào)用的finished和異步請求的一樣,都是從運行隊列移除當前Call券时,然后去等待隊列取消息,得到則處理伏伯,取不到則不處理橘洞。

總結一下分發(fā)器Dispatcher的作用:內(nèi)部維護隊列與線程池,完成請求調(diào)配说搅。所有邏輯大部分都集中在攔截器中炸枣,但進入攔截器之前還需要依靠分發(fā)器來調(diào)配請求任務。

攔截器

下面講講OkHttp重點的攔截器

剛才說到getResponseWithInterceptorChain最終會得到響應結果弄唧,也就是說攔截器這塊是真正進行網(wǎng)絡請求的地方适肠。攔截器采用的是責任鏈模式,什么是責任鏈模式呢候引?舉個例子假如你要點外賣:A是你本人侯养,B是騎手,C是商家
A只負責點外賣和收外賣澄干,不用考慮商家是怎么做的
B是收到訂單后去商家拿外賣逛揩,然后送到你手里
C是把外賣打包好,然后拿給騎手

其實這就是一個U型操作麸俘,一條鏈回去一條鏈回來辩稽,并且除了最后一個外,都有一個前置从媚、中置和后置操作逞泄,最后一個結合起來只是把外賣拿給騎手的操作,也就是中置操作拜效。前置操作是準備喷众,中置操作是往下傳,后置操作是得到結果后做處理拂檩,再往回傳這么一個操作流程侮腹。如果還是不太清楚的話去搜下責任鏈模式的例子。

而在OkHttp攔截器中稻励,就是采用這么一個模式父阻,把所有的攔截器都串起來執(zhí)行愈涩。

  //RealCall.kt
  @Throws(IOException::class)
  fun getResponseWithInterceptorChain(): Response {
    // Build a full stack of interceptors.
    //1
    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)
    //2
    val chain = RealInterceptorChain(interceptors, transmitter, null, 0, originalRequest, this,
        client.connectTimeoutMillis, client.readTimeoutMillis, client.writeTimeoutMillis)

    var calledNoMoreExchanges = false
    try {
      //3
      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)
      }
    }
  }

分析:

  • 1:把所有需要用的攔截器串起來存在mutableList里面,為什么是可變的List加矛?因為client.interceptors和client.networkInterceptors我們可以自定義自己的攔截器添加進去履婉,所以這兩個的區(qū)別就在于client.interceptors攔截器在整條鏈的鏈頭,而client.networkInterceptors是在鏈尾的前一個(CallServerInterceptor才是鏈尾)
  • 2:得到一條鏈子之后斟览,需要對它進行包裝毁腿,主要目的是出入一些參數(shù)后續(xù)使用,例如剛開始的請求數(shù)據(jù)originalRequest苛茂,超時時間等等...
  • 3:前面還是準備工作已烤,調(diào)用chain.proceed(originalRequest)才真正執(zhí)行,這條鏈才開始動起來妓羊。為什么能動起來胯究?往下看
  //RealInterceptorChain.kt
  @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.
    //1
    val next = RealInterceptorChain(interceptors, transmitter, exchange,
        index + 1, request, call, connectTimeout, readTimeout, writeTimeout)
    val interceptor = interceptors[index]
    //2
    @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
  }

分析:

  • 1:首先先建立一條鏈子next ,其實值都沒變躁绸,只有把索引值加一裕循,這樣鏈子就開始往下;并取到第一個攔截器interceptor (index剛傳進來的時候為0)
  • 2:interceptor.intercept(next)净刮,執(zhí)行剛才的第一個攔截器的intercept方法并傳了下一條鏈子進去剥哑。

鏈子組裝好了,并且也轉起來了淹父,那就來看看那些攔截器是干什么的吧(只講標注出來的重點株婴,其他關系不大的代碼可以不看)

重定向攔截器RetryAndFollowUpInterceptor

在不添加攔截器的情況下,第一個攔截器是RetryAndFollowUpInterceptor攔截器弹灭,根據(jù)命名可以猜到是關于重試的攔截器督暂。其實它是重試和重定向攔截器:

  @Throws(IOException::class)
  override fun intercept(chain: Interceptor.Chain): Response {
    var request = chain.request()
    val realChain = chain as RealInterceptorChain
    val transmitter = realChain.transmitter()
    var followUpCount = 0
    var priorResponse: Response? = null
    //1
    while (true) {
      transmitter.prepareToConnect(request)

      if (transmitter.isCanceled) {
        throw IOException("Canceled")
      }

      var response: Response
      var success = false
      try {
        //2
        response = realChain.proceed(request, transmitter, null)
        success = true
      } catch (e: RouteException) {
        //3
        // The attempt to connect via a route failed. The request will not have been sent.
        if (!recover(e.lastConnectException, transmitter, false, request)) {
          throw e.firstConnectException
        }
        continue
      } catch (e: IOException) {
        //4
        // An attempt to communicate with a server failed. The request may have been sent.
        val requestSendStarted = e !is ConnectionShutdownException
        if (!recover(e, transmitter, requestSendStarted, request)) throw e
        continue
      } finally {
        // The network call threw an exception. Release any resources.
        if (!success) {
          transmitter.exchangeDoneDueToException()
        }
      }
      //5
      // Attach the prior response if it exists. Such responses never have a body.
      if (priorResponse != null) {
        response = response.newBuilder()
            .priorResponse(priorResponse.newBuilder()
                .body(null)
                .build())
            .build()
      }

      val exchange = response.exchange
      val route = exchange?.connection()?.route()
      val followUp = followUpRequest(response, route)
      //6
      if (followUp == null) {
        if (exchange != null && exchange.isDuplex) {
          transmitter.timeoutEarlyExit()
        }
        return response
      }

      val followUpBody = followUp.body
      if (followUpBody != null && followUpBody.isOneShot()) {
        return response
      }

      response.body?.closeQuietly()
      if (transmitter.hasExchange()) {
        exchange?.detachWithViolence()
      }

      if (++followUpCount > MAX_FOLLOW_UPS) {
        throw ProtocolException("Too many follow-up requests: $followUpCount")
      }

      request = followUp
      priorResponse = response
    }
  }

剛才提到責任鏈是有前置操作、中置操作和后置操作穷吮,那這個攔截器的這些操作是什么逻翁?邊分析邊總結
分析:

  • 1:while(true),不斷的去取response捡鱼,直到取到了才返回出來八回,不然一直在里面去取去重試,這就是為什么能重試了驾诈。
  • 2:realChain.proceed缠诅,把請求request傳給下一個攔截器,所以這個是中置操作
  • 3和4:都是對請求失敗的處理乍迄,從這里開始就是后置操作了管引,這里已經(jīng)得到結果了,如果請求失敗就重試闯两,然后continue重新開始請求褥伴。
  • 5:當需要重定向的時候谅将,會得到重定向信息,然后又開始新一輪的請求重慢,只不過這次用的是重定向會回來后的請求饥臂。
  • 6:當我不用重試涯塔,也不用重定向了魂莫,那就是拿到最終的response了,返回給外部處理蘸鲸。

那前置工作是什么核芽?
看到剛開始的transmitter.prepareToConnect(request)囚戚,這是對請求前的準備和檢查,里面涉及到了ExchangeFinder狞洋,其實就是創(chuàng)建一個ExchangeFinder對象弯淘,賦值給exchangeFinder,后面在連接攔截器的時候會講到吉懊。

  //Transmitter.kt
  fun prepareToConnect(request: Request) {
    if (this.request != null) {
      if (this.request!!.url.canReuseConnectionFor(request.url) && exchangeFinder!!.hasRouteToTry()) {
        return // Already ready.
      }
      check(exchange == null)

      if (exchangeFinder != null) {
        maybeReleaseConnection(null, true)
        exchangeFinder = null
      }
    }

    this.request = request
    this.exchangeFinder = ExchangeFinder(
        this, connectionPool, createAddress(request.url), call, eventListener)
  }

總結一下重定向攔截器的工作:
前置:對請求進行準備和檢查
中置:準備工作完后傳給下一個攔截器
后置:從下個攔截器接收到的信息做處理(重試、重定向或者放回response)

橋接攔截器BridgeInterceptor

接下來看看下一個攔截器假勿,BridgeInterceptor橋接攔截器

  //BridgeInterceptor.kt
  @Throws(IOException::class)
  override fun intercept(chain: Interceptor.Chain): Response {
    val userRequest = chain.request()
    val requestBuilder = userRequest.newBuilder()
    //1
    val body = userRequest.body
    if (body != null) {
      val contentType = body.contentType()
      if (contentType != null) {
        requestBuilder.header("Content-Type", contentType.toString())
      }

      val contentLength = body.contentLength()
      if (contentLength != -1L) {
        requestBuilder.header("Content-Length", contentLength.toString())
        requestBuilder.removeHeader("Transfer-Encoding")
      } else {
        requestBuilder.header("Transfer-Encoding", "chunked")
        requestBuilder.removeHeader("Content-Length")
      }
    }

    if (userRequest.header("Host") == null) {
      requestBuilder.header("Host", userRequest.url.toHostHeader())
    }

    if (userRequest.header("Connection") == null) {
      requestBuilder.header("Connection", "Keep-Alive")
    }

    // If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
    // the transfer stream.
    var transparentGzip = false
    if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
      transparentGzip = true
      requestBuilder.header("Accept-Encoding", "gzip")
    }

    val cookies = cookieJar.loadForRequest(userRequest.url)
    if (cookies.isNotEmpty()) {
      requestBuilder.header("Cookie", cookieHeader(cookies))
    }

    if (userRequest.header("User-Agent") == null) {
      requestBuilder.header("User-Agent", userAgent)
    }
    //2
    val networkResponse = chain.proceed(requestBuilder.build())
    //3
    cookieJar.receiveHeaders(userRequest.url, networkResponse.headers)

    val responseBuilder = networkResponse.newBuilder()
        .request(userRequest)

    if (transparentGzip &&
        "gzip".equals(networkResponse.header("Content-Encoding"), ignoreCase = true) &&
        networkResponse.promisesBody()) {
      val responseBody = networkResponse.body
      if (responseBody != null) {
        val gzipSource = GzipSource(responseBody.source())
        val strippedHeaders = networkResponse.headers.newBuilder()
            .removeAll("Content-Encoding")
            .removeAll("Content-Length")
            .build()
        responseBuilder.headers(strippedHeaders)
        val contentType = networkResponse.header("Content-Type")
        responseBuilder.body(RealResponseBody(contentType, -1L, gzipSource.buffer()))
      }
    }

    return responseBuilder.build()
  }

這個攔截器主要是對我們的請求進一步的包裝借嗽,因為一開始我們的請求只有一條地址路徑,而這個攔截器能解析我們請求转培,并加上一些重要的請求頭信息恶导,這樣我們就不用了手動加上或者本來不知道的請求頭信息(Content-Length等等)。
分析:

  • 1:首先在原始請求的基礎上浸须,加上了一些請求頭信息惨寿,組裝成完整的請求信息,還添加上了gzip解析(壓縮能增加傳輸速度)删窒,這是前置工作裂垦。
  • 2:把組裝好的請求頭信息傳到下一個攔截器去處理,這是中置工作肌索。
  • 3:從下個攔截器得到的response做處理蕉拢,假如支持gzip解析,則解析并返回給上一個攔截器(重試和重定向攔截器)诚亚,否則直接返回上一個攔截器晕换。這是后置工作。

緩存攔截器CacheInterceptor

接下來看下一個攔截器站宗,CacheInterceptor緩存攔截器闸准。緩存攔截器本身主要邏輯其實都在緩存策略中,攔截器本身邏輯非常簡單梢灭,所以先來講下緩存攔截器中的緩存策略CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()夷家,它實際返回一個帶有networkRequest和cacheResponse的CacheStrategy緩存策略:

    //CacheStrategy.kt
    init {
      //1
      if (cacheResponse != null) {
        this.sentRequestMillis = cacheResponse.sentRequestAtMillis
        this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis
        val headers = cacheResponse.headers
        //2
        for (i in 0 until headers.size) {
          val fieldName = headers.name(i)
          val value = headers.value(i)
          when {
            fieldName.equals("Date", ignoreCase = true) -> {
              servedDate = value.toHttpDateOrNull()
              servedDateString = value
            }
            fieldName.equals("Expires", ignoreCase = true) -> {
              expires = value.toHttpDateOrNull()
            }
            fieldName.equals("Last-Modified", ignoreCase = true) -> {
              lastModified = value.toHttpDateOrNull()
              lastModifiedString = value
            }
            fieldName.equals("ETag", ignoreCase = true) -> {
              etag = value
            }
            fieldName.equals("Age", ignoreCase = true) -> {
              ageSeconds = value.toNonNegativeInt(-1)
            }
          }
        }
      }
    }

分析:

  • 1:若存在候選緩存蒸其,獲取該response請求時發(fā)起和接收的時間(在CallServerInterceptor中記錄的這些時間)
  • 2:遍歷header,保存Date瘾英、Expires枣接、Last-Modified、ETag缺谴、Age等緩存機制相關字段的值
    //CacheStrategy.kt
    private fun computeCandidate(): CacheStrategy {
      //1
      // No cached response.
      if (cacheResponse == null) {
        return CacheStrategy(request, null)
      }
      //2
      // Drop the cached response if it's missing a required handshake.
      if (request.isHttps && cacheResponse.handshake == null) {
        return CacheStrategy(request, null)
      }
      //3
      // If this response shouldn't have been stored, it should never be used as a response source.
      // This check should be redundant as long as the persistence store is well-behaved and the
      // rules are constant.
      if (!isCacheable(cacheResponse, request)) {
        return CacheStrategy(request, null)
      }
      //4
      val requestCaching = request.cacheControl
      if (requestCaching.noCache || hasConditions(request)) {
        return CacheStrategy(request, null)
      }

      val responseCaching = cacheResponse.cacheControl

      val ageMillis = cacheResponseAge()
      var freshMillis = computeFreshnessLifetime()

      if (requestCaching.maxAgeSeconds != -1) {
        freshMillis = minOf(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds.toLong()))
      }

      var minFreshMillis: Long = 0
      if (requestCaching.minFreshSeconds != -1) {
        minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds.toLong())
      }

      var maxStaleMillis: Long = 0
      if (!responseCaching.mustRevalidate && requestCaching.maxStaleSeconds != -1) {
        maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds.toLong())
      }
      //5
      if (!responseCaching.noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
        val builder = cacheResponse.newBuilder()
        if (ageMillis + minFreshMillis >= freshMillis) {
          builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"")
        }
        val oneDayMillis = 24 * 60 * 60 * 1000L
        if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
          builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"")
        }
        return CacheStrategy(null, builder.build())
      }
      //6
      // Find a condition to add to the request. If the condition is satisfied, the response body
      // will not be transmitted.
      val conditionName: String
      val conditionValue: String?
      when {
        etag != null -> {
          conditionName = "If-None-Match"
          conditionValue = etag
        }

        lastModified != null -> {
          conditionName = "If-Modified-Since"
          conditionValue = lastModifiedString
        }

        servedDate != null -> {
          conditionName = "If-Modified-Since"
          conditionValue = servedDateString
        }

        else -> return CacheStrategy(request, null) // No condition! Make a regular request.
      }

      val conditionalRequestHeaders = request.headers.newBuilder()
      conditionalRequestHeaders.addLenient(conditionName, conditionValue!!)
      //7
      val conditionalRequest = request.newBuilder()
          .headers(conditionalRequestHeaders.build())
          .build()
      return CacheStrategy(conditionalRequest, cacheResponse)
    }

分析:

  • 1:cacheResponse 是從緩存中找到的響應但惶,如果為null,那就表示沒有找到對應的緩存湿蛔,創(chuàng)建的 CacheStrategy 實例膀曾,對象只存在 networkRequest ,這代表了需要發(fā)起網(wǎng)絡請求阳啥。
  • 2:繼續(xù)往下走意味著 cacheResponse 必定存在添谊,但是它不一定能用。后續(xù)進行有效性的一系列判斷察迟,如果本次請求是HTTPS斩狱,但是緩存中沒有對應的握手信息,那么緩存無效扎瓶。
  • 3:isCacheable方法通過response的狀態(tài)碼以及response所踊、request的緩存控制字段來判斷是否可緩存。緩存響應中的響應碼為 200, 203, 204, 300, 301, 404, 405, 410, 414, 501, 308 的情況下,只判斷服務器是不是給了Cache-Control: no-store (資源不能被緩存)概荷,所以如果服務器給到了這個響應頭秕岛,那就和前面兩個判定一致(緩存不可用)。否則繼續(xù)進一步判斷緩存是否可用
  • 4:判斷request中的緩存控制字段和header是否設置If-Modified-Since或If-None-Match误证。走到這一步继薛,OkHttp需要先對用戶本次發(fā)起的 Request 進行判定,如果用戶指定了 Cache-Control: no-cache (不使用緩存)的請求頭或者請求頭包含 If-Modified-Since 或 If-None-Match (請求驗證)愈捅,那么就不允許使用緩存遏考。
  • 5:判斷緩存是否在有效期內(nèi)。
  • 6:緩存超出有效期改鲫,判斷是否設置了Etag和Last-Modified诈皿。如果都沒有,意味著無法與服務器發(fā)起比較像棘,只能重新請求稽亏。如果有,則添加請求頭缕题,Etag對應If-None-Match截歉、Last-Modified對應If-Modified-Since
  • 7:至此,緩存的判定結束烟零,攔截器中只需要判斷 CacheStrategy 中 networkRequest 與 cacheResponse 的不同組合就能夠判斷是否允許使用緩存瘪松。

注意:但是需要注意的是咸作,如果用戶在創(chuàng)建請求時,配置了 onlyIfCached 這意味著用戶這次希望這個請求只從緩存獲得宵睦,不需要發(fā)起請求记罚。那如果生成的 CacheStrategy 存在 networkRequest 這意味著肯定會發(fā)起請求,此時出現(xiàn)沖突壳嚎!那會直接給到攔截器一個既沒有 networkRequest 又沒有 cacheResponse 的對象桐智。攔截器直接返回用戶 504 !


緩存策略

講完緩存策略現(xiàn)在來看緩存攔截器就很簡單了:

    //CacheStrategy.kt
    fun compute(): CacheStrategy {
      val candidate = computeCandidate()

      // We're forbidden from using the network and the cache is insufficient.
      if (candidate.networkRequest != null && request.cacheControl.onlyIfCached) {
        return CacheStrategy(null, null)
      }

      return candidate
    }

總結下緩存策略:
1烟馅、如果從緩存獲取的 Response 是null说庭,那就需要使用網(wǎng)絡請求獲取響應;
2郑趁、如果是Https請求刊驴,但是又丟失了握手信息,那也不能使用緩存寡润,需要進行網(wǎng)絡請求捆憎;
3、如果判斷響應碼不能緩存且響應頭有 no-store 標識梭纹,那就需要進行網(wǎng)絡請求攻礼;
4、如果請求頭有 no-cache 標識或者有 If-Modified-Since/If-None-Match 栗柒,那么需要進行網(wǎng)絡請求;
5知举、如果響應頭沒有 no-cache 標識瞬沦,且緩存時間沒有超過極限時間,那么可以使用緩存雇锡,不需要進行網(wǎng)絡請求逛钻;
6、如果緩存過期了锰提,判斷響應頭是否設置 Etag/Last-Modified/Date 曙痘,沒有那就直接使用網(wǎng)絡請求否則需要考慮服務器返回304;

并且立肘,只要需要進行網(wǎng)絡請求边坤,請求頭中就不能包含 only-if-cached ,否則框架直接返回504谅年!

  //CacheInterceptor.kt
  @Throws(IOException::class)
  override fun intercept(chain: Interceptor.Chain): Response {
    //1
    val cacheCandidate = cache?.get(chain.request())

    val now = System.currentTimeMillis()
    //2
    val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()
    val networkRequest = strategy.networkRequest
    val cacheResponse = strategy.cacheResponse

    cache?.trackResponse(strategy)

    if (cacheCandidate != null && cacheResponse == null) {
      // The cache candidate wasn't applicable. Close it.
      cacheCandidate.body?.closeQuietly()
    }
    //3
    // If we're forbidden from using the network and the cache is insufficient, fail.
    if (networkRequest == null && cacheResponse == null) {
      return Response.Builder()
          .request(chain.request())
          .protocol(Protocol.HTTP_1_1)
          .code(HTTP_GATEWAY_TIMEOUT)
          .message("Unsatisfiable Request (only-if-cached)")
          .body(EMPTY_RESPONSE)
          .sentRequestAtMillis(-1L)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build()
    }
    //4
    // If we don't need the network, we're done.
    if (networkRequest == null) {
      return cacheResponse!!.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build()
    }

    var networkResponse: Response? = null
    try {
      //5
      networkResponse = chain.proceed(networkRequest)
    } finally {
      // If we're crashing on I/O or otherwise, don't leak the cache body.
      if (networkResponse == null && cacheCandidate != null) {
        cacheCandidate.body?.closeQuietly()
      }
    }
    //6
    // If we have a cache response too, then we're doing a conditional get.
    if (cacheResponse != null) {
      if (networkResponse?.code == HTTP_NOT_MODIFIED) {
        val response = cacheResponse.newBuilder()
            .headers(combine(cacheResponse.headers, networkResponse.headers))
            .sentRequestAtMillis(networkResponse.sentRequestAtMillis)
            .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis)
            .cacheResponse(stripBody(cacheResponse))
            .networkResponse(stripBody(networkResponse))
            .build()

        networkResponse.body!!.close()

        // Update the cache after combining headers but before stripping the
        // Content-Encoding header (as performed by initContentStream()).
        cache!!.trackConditionalCacheHit()
        cache.update(cacheResponse, response)
        return response
      } else {
        cacheResponse.body?.closeQuietly()
      }
    }

    val response = networkResponse!!.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build()
    //7
    if (cache != null) {
      if (response.promisesBody() && CacheStrategy.isCacheable(response, networkRequest)) {
        // Offer this request to the cache.
        val cacheRequest = cache.put(response)
        return cacheWritingResponse(cacheRequest, response)
      }

      if (HttpMethod.invalidatesCache(networkRequest.method)) {
        try {
          cache.remove(networkRequest)
        } catch (_: IOException) {
          // The cache cannot be written.
        }
      }
    }

    return response
  }

緩存攔截器說復雜不復雜茧痒,說簡單不簡單,把流程縷清就好了融蹂。

  • 強制緩存旺订,即緩存在有效期內(nèi)就直接返回緩存弄企,不進行網(wǎng)絡請求。
  • 對比緩存区拳,即緩存超過有效期拘领,進行網(wǎng)絡請求。若數(shù)據(jù)未修改樱调,服務端返回不帶body的304響應约素,表示客戶端緩存仍有效可用;否則返回完整最新數(shù)據(jù)本涕,客戶端取網(wǎng)絡請求的最新數(shù)據(jù)业汰。

分析:

  • 1:cache即構建OkHttpClient時傳入的Cache對象的內(nèi)部成員,用request的url作為key菩颖,查找緩存的response作為候選緩存样漆,當沒傳Cache對象時,cacheCandidate 為空晦闰,cacheCandidate = null
  • 2:生成緩存策略放祟,獲取緩存策略生成的networkRequest和cacheResponse(合法緩存),networkRequest是否為空決定是否請求網(wǎng)絡呻右,cacheResponse是否為空決定是否使用緩存跪妥,下面流程會根據(jù)生成的networkRequest和cacheResponse來決定執(zhí)行什么緩存策略。
  • 3:即無網(wǎng)絡請求又無合法緩存声滥,返回狀態(tài)碼504的response(HTTP_GATEWAY_TIMEOUT = 504),
  • 4:強制緩存策略
  • 5:調(diào)用下一層攔截器
  • 6:當服務端放回networkResponse后眉撵,執(zhí)行對比緩存策略,服務端返回304狀態(tài)碼(HTTP_NOT_MODIFIED = 304)落塑,表示本地緩存仍有效
  • 7:判斷能否緩存networkResponse纽疟,實現(xiàn)本次緩存

總結一下:
networkRequest、cacheResponse均為空憾赁,構建返回一個狀態(tài)碼為504的response污朽;
networkRequest為空、cacheResponse不為空龙考,執(zhí)行強制緩存
networkRequest蟆肆、cacheResponse均不為空,執(zhí)行對比緩存
networkRequest不為空晦款、cacheResponse為空炎功,網(wǎng)絡請求獲取到最新response后,視情況緩存response

緩存只能緩存GET請求的響應

緩存攔截器本身主要邏輯其實都在緩存策略中柬赐,攔截器本身邏輯非常簡單亡问,如果確定需要發(fā)起網(wǎng)絡請求,則下一個攔截器為 ConnectInterceptor

連接攔截器ConnectInterceptor

接著下一個ConnectInterceptor連接攔截器

  //ConnectInterceptor.kt
  @Throws(IOException::class)
  override fun intercept(chain: Interceptor.Chain): Response {
    val realChain = chain as RealInterceptorChain
    val request = realChain.request()
    val transmitter = realChain.transmitter()

    // We need the network to satisfy this request. Possibly for validating a conditional GET.
    val doExtensiveHealthChecks = request.method != "GET"
    val exchange = transmitter.newExchange(chain, doExtensiveHealthChecks)

    return realChain.proceed(request, transmitter, exchange)
  }

代碼也是很簡單,但里面的邏輯很多州藕,只有前置和中置操作束世,沒有后置操作,因為這里是建立一個連接去發(fā)請求的床玻,所以沒有后置工作毁涉。這個攔截器中最重要的是調(diào)用newExchange然后new一個exchange出來傳給下一個攔截器

  //Transmitter.kt
  internal fun newExchange(chain: Interceptor.Chain, doExtensiveHealthChecks: Boolean): Exchange {
    synchronized(connectionPool) {
      check(!noMoreExchanges) { "released" }
      check(exchange == null) {
        "cannot make a new request because the previous response is still open: " +
            "please call response.close()"
      }
    }
    //1
    val codec = exchangeFinder!!.find(client, chain, doExtensiveHealthChecks)
    //2
    val result = Exchange(this, call, eventListener, exchangeFinder!!, codec)

    synchronized(connectionPool) {
      this.exchange = result
      this.exchangeRequestDone = false
      this.exchangeResponseDone = false
      return result
    }
  }

重點只看1、2锈死,分析:

  • 1:調(diào)用exchangeFinder.find得到一個codec(code & decoder)贫堰,連接的編碼解碼器,而這個exchangeFinder就是在重試和重定向攔截器做前置工作時候準備的待牵,在這里就用上了其屏。
  • 2:然后得到編碼解碼器codec后拼成一個Exchange返回出去。

注意:編碼解碼器就是在發(fā)請求報文和接收請求報文需要按照格式去讀缨该,可以是http1格式也可以是http2格式偎行,不同格式就是不用編碼。

看看怎么得到codec的

  //ExchangeFinder.kt
  fun find(
    client: OkHttpClient,
    chain: Interceptor.Chain,
    doExtensiveHealthChecks: Boolean
  ): ExchangeCodec {
    val connectTimeout = chain.connectTimeoutMillis()
    val readTimeout = chain.readTimeoutMillis()
    val writeTimeout = chain.writeTimeoutMillis()
    val pingIntervalMillis = client.pingIntervalMillis
    val connectionRetryEnabled = client.retryOnConnectionFailure

    try {
      //1
      val resultConnection = findHealthyConnection(
          connectTimeout = connectTimeout,
          readTimeout = readTimeout,
          writeTimeout = writeTimeout,
          pingIntervalMillis = pingIntervalMillis,
          connectionRetryEnabled = connectionRetryEnabled,
          doExtensiveHealthChecks = doExtensiveHealthChecks
      )
      //2
      return resultConnection.newCodec(client, chain)
    } catch (e: RouteException) {
      trackFailure()
      throw e
    } catch (e: IOException) {
      trackFailure()
      throw RouteException(e)
    }
  }

得到codec需要兩個步驟:

  • 1:獲取一個健康的連接贰拿。
  • 2:然后用健康的鏈接根據(jù)client和chain創(chuàng)建一個對應的編碼解碼器蛤袒。

怎么得到一個健康的連接呢?

  //ExchangeFinder.kt
  @Throws(IOException::class)
  private fun findHealthyConnection(
    connectTimeout: Int,
    readTimeout: Int,
    writeTimeout: Int,
    pingIntervalMillis: Int,
    connectionRetryEnabled: Boolean,
    doExtensiveHealthChecks: Boolean
  ): RealConnection {
    while (true) {
      //1
      val candidate = findConnection(
          connectTimeout = connectTimeout,
          readTimeout = readTimeout,
          writeTimeout = writeTimeout,
          pingIntervalMillis = pingIntervalMillis,
          connectionRetryEnabled = connectionRetryEnabled
      )

      // If this is a brand new connection, we can skip the extensive health checks.
      synchronized(connectionPool) {
        if (candidate.successCount == 0) {
          return candidate
        }
      }

      // Do a (potentially slow) check to confirm that the pooled connection is still good. If it
      // isn't, take it out of the pool and start again.
      //2
      if (!candidate.isHealthy(doExtensiveHealthChecks)) {
        candidate.noNewExchanges()
        continue
      }

      return candidate
    }
  }

分析:

  • 1:創(chuàng)建一個連接出來膨更。
  • 2:然后判斷健不健康妙真,不健康的話就繼續(xù)continue(注意while),直到取到一個健康的連接返回出去荚守。

注意:我們的目的是要得到一個編碼解碼器codec才引申出健康的連接珍德。

那么怎么創(chuàng)建一個連接呢?

  //ExchangeFinder.kt
  @Throws(IOException::class)
  private fun findConnection(
    connectTimeout: Int,
    readTimeout: Int,
    writeTimeout: Int,
    pingIntervalMillis: Int,
    connectionRetryEnabled: Boolean
  ): RealConnection {
    var foundPooledConnection = false
    var result: RealConnection? = null
    var selectedRoute: Route? = null
    var releasedConnection: RealConnection?
    val toClose: Socket?
    synchronized(connectionPool) {
      if (transmitter.isCanceled) throw IOException("Canceled")
      hasStreamFailure = false // This is a fresh attempt.
      //1
      releasedConnection = transmitter.connection
      toClose = if (transmitter.connection != null && transmitter.connection!!.noNewExchanges) {
        transmitter.releaseConnectionNoEvents()
      } else {
        null
      }
      //2
      if (transmitter.connection != null) {
        // We had an already-allocated connection and it's good.
        result = transmitter.connection
        releasedConnection = null
      }
      //3
      if (result == null) {
        // Attempt to get a connection from the pool.
        //4
        if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {
          foundPooledConnection = true
          result = transmitter.connection
        } else if (nextRouteToTry != null) {
          selectedRoute = nextRouteToTry
          nextRouteToTry = null
        } else if (retryCurrentRoute()) {
          selectedRoute = transmitter.connection!!.route()
        }
      }
    }
    toClose?.closeQuietly()

    if (releasedConnection != null) {
      eventListener.connectionReleased(call, releasedConnection!!)
    }
    if (foundPooledConnection) {
      eventListener.connectionAcquired(call, result!!)
    }
    //5
    if (result != null) {
      // If we found an already-allocated or pooled connection, we're done.
      return result!!
    }

    // If we need a route selection, make one. This is a blocking operation.
    var newRouteSelection = false
    if (selectedRoute == null && (routeSelection == null || !routeSelection!!.hasNext())) {
      newRouteSelection = true
      routeSelection = routeSelector.next()
    }

    var routes: List<Route>? = null
    synchronized(connectionPool) {
      if (transmitter.isCanceled) throw IOException("Canceled")

      if (newRouteSelection) {
        // Now that we have a set of IP addresses, make another attempt at getting a connection from
        // the pool. This could match due to connection coalescing.
        routes = routeSelection!!.routes
        //6
        if (connectionPool.transmitterAcquirePooledConnection(
                address, transmitter, routes, false)) {
          foundPooledConnection = true
          result = transmitter.connection
        }
      }

      if (!foundPooledConnection) {
        if (selectedRoute == null) {
          selectedRoute = routeSelection!!.next()
        }

        // Create a connection and assign it to this allocation immediately. This makes it possible
        // for an asynchronous cancel() to interrupt the handshake we're about to do.
        //7
        result = RealConnection(connectionPool, selectedRoute!!)
        connectingConnection = result
      }
    }

    // If we found a pooled connection on the 2nd time around, we're done.
    if (foundPooledConnection) {
      eventListener.connectionAcquired(call, result!!)
      return result!!
    }
    //8
    // Do TCP + TLS handshakes. This is a blocking operation.
    result!!.connect(
        connectTimeout,
        readTimeout,
        writeTimeout,
        pingIntervalMillis,
        connectionRetryEnabled,
        call,
        eventListener
    )
    connectionPool.routeDatabase.connected(result!!.route())

    var socket: Socket? = null
    synchronized(connectionPool) {
      connectingConnection = null
      // Last attempt at connection coalescing, which only occurs if we attempted multiple
      // concurrent connections to the same host.
      //9
      if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)) {
        // We lost the race! Close the connection we created and return the pooled connection.
        result!!.noNewExchanges = true
        socket = result!!.socket()
        result = transmitter.connection

        // It's possible for us to obtain a coalesced connection that is immediately unhealthy. In
        // that case we will retry the route we just successfully connected with.
        nextRouteToTry = selectedRoute
      } else {
        //10
        connectionPool.put(result!!)
        transmitter.acquireConnectionNoEvents(result!!)
      }
    }
    socket?.closeQuietly()

    eventListener.connectionAcquired(call, result!!)
    return result!!
  }

代碼比較長矗漾,只分析重點
分析:
因為初次進來的時候菱阵,所有都是默認值,都為空缩功,所以1和2中的connection為空,直接看到3都办,之后再看1和2嫡锌。

  • 3:剛進來的時候result初始值為空,來到了4.
  • 4:調(diào)用連接池connectionPool的transmitterAcquirePooledConnection琳钉,主要是去連接池里面拿連接势木。
  //RealConnectionPool.kt
  fun transmitterAcquirePooledConnection(
    address: Address,
    transmitter: Transmitter,
    routes: List<Route>?,
    requireMultiplexed: Boolean
  ): Boolean {
    this.assertThreadHoldsLock()

    for (connection in connections) {
      if (requireMultiplexed && !connection.isMultiplexed) continue
      if (!connection.isEligible(address, routes)) continue
      transmitter.acquireConnectionNoEvents(connection)
      return true
    }
    return false
  }

循環(huán)遍歷連接池里面的連接,因為剛才4傳進來的requireMultiplexed = false歌懒,所以往下走啦桌,只要connection.isEligible(address, routes)為true,代表這個可用,那我就直接拿出這條連接來使用甫男,acquireConnectionNoEvents(connection)賦值到Transmitter的connection且改,下次就用到的時候就不為空了。

注意:我們的目的是要得到一個編碼解碼器codec才引申出健康的連接再到怎么取連接板驳。

那么問題又來了又跛,什么才算可用的連接呢?

  //RealConnection.kt
  internal fun isEligible(address: Address, routes: List<Route>?): Boolean {
    //1
    // If this connection is not accepting new exchanges, we're done.
    if (transmitters.size >= allocationLimit || noNewExchanges) return false
    //2
    // If the non-host fields of the address don't overlap, we're done.
    if (!this.route.address.equalsNonHost(address)) return false
    //3
    // If the host exactly matches, we're done: this connection can carry the address.
    if (address.url.host == this.route().address.url.host) {
      return true // This connection is a perfect match.
    }

    // At this point we don't have a hostname match. But we still be able to carry the request if
    // our connection coalescing requirements are met. See also:
    // https://hpbn.co/optimizing-application-delivery/#eliminate-domain-sharding
    // https://daniel.haxx.se/blog/2016/08/18/http2-connection-coalescing/

    // 1. This connection must be HTTP/2.
    if (http2Connection == null) return false

    // 2. The routes must share an IP address.
    if (routes == null || !routeMatchesAny(routes)) return false

    // 3. This connection's server certificate's must cover the new host.
    if (address.hostnameVerifier !== OkHostnameVerifier) return false
    if (!supportsUrl(address.url)) return false

    // 4. Certificate pinning must match the host.
    try {
      address.certificatePinner!!.check(address.url.host, handshake()!!.peerCertificates)
    } catch (_: SSLPeerUnverifiedException) {
      return false
    }

    return true // The caller's address can be carried by this connection.
  }

這里可分為兩部分若治,一部分是http1的慨蓝,一部分是http2的。
分析:

  • 1:Http1的情況系allocationLimit為1端幼,Http2的情況下allocationLimit為4礼烈,transmitters.size是連接的請求數(shù)量,在http1時婆跑,每個http連接只能接受一個請求此熬,noNewExchanges代表目前連接是否還接受新的請求。所以洽蛀,假如我還接受別的請求摹迷,并且算上這個請求沒有超出allocationLimit,繼續(xù)往下走郊供,否則繼續(xù)返回false峡碉。
  • 2:現(xiàn)在的這個請求是否與連接的請求是否連在同個地方,也就是一下這些值要相等驮审,包括端口以及加密信息鲫寄、代理配置等等...
  //Address.kt
  internal fun equalsNonHost(that: Address): Boolean {
    return this.dns == that.dns &&
        this.proxyAuthenticator == that.proxyAuthenticator &&
        this.protocols == that.protocols &&
        this.connectionSpecs == that.connectionSpecs &&
        this.proxySelector == that.proxySelector &&
        this.proxy == that.proxy &&
        this.sslSocketFactory == that.sslSocketFactory &&
        this.hostnameVerifier == that.hostnameVerifier &&
        this.certificatePinner == that.certificatePinner &&
        this.url.port == that.url.port
  }
  • 3:目標主機名與連接的主機名相同就返回

可能你會覺得,這樣做就可以判斷可不可用了疯淫,那下面代碼是干嘛的地来?主要是針對Http2的(包括是Https的情況下),其實還有一種叫連接合并(connection coalescing)的東西熙掺,這種情況是不同域名指向了同一主機IP未斑,這種情況是存在的,所以我們可以不要求域名要一致币绩。但是怎么保證我們解析到相同IP能正確訪問到我們的想訪問的主機呢蜡秽?可以看他們的證書簽名。

假如域名不一樣缆镣,繼續(xù)往下看芽突,這個連接必須是Http2的情況下才可能存在可用的鏈接,否則直接返回false董瞻。接著routes(Route后面會講到)不為空并且IP和代理模式要一樣寞蚌,才會往下走,但是第一次傳進來的時候routes是空的,所以第一次如果不在前面取到可用鏈接的話挟秤,后面就直接返回壹哺,然后回到findConnection里面繼續(xù)往下走;所以routes是判斷是否能連接合并煞聪,假如都有了斗躏,那往下走,證書簽名昔脯、端口也要一樣才能算一個可用的連接啄糙。

所以isEligible中,可用的連接是:我的請求個數(shù)沒超限云稚,并且用同樣的方式連接同一個主機隧饼。

注意:我們的目的是要得到一個編碼解碼器codec才引申出健康的連接再到怎么取連接。

回到剛才長長代碼的findConnection里面繼續(xù)静陈,現(xiàn)在我們就知道了怎么去連接池里面取一個可用的鏈接了燕雁,接著往下:

  • 5:假如4取到后可用的連接后,之后返回鲸拥,下面就不走了拐格,出去判斷健不健康了。
  • 6:如果在第一次取的時候拿不到刑赶,那么再去拿一次捏浊,只不過這次對比第一次多了個routes,那么就講一下Address和Route是什么撞叨?
    Address:里面兩個信息金踪,一個是主機名,一個是端口牵敷,有了這個就可以直接解析得到IP然后結合代理類型找到真的代理的主機胡岔。而這個Address是在第一個攔截器準備工作創(chuàng)建exchangeFinder的時候傳進去的。Address除了主機名uriHost和端口uriPort枷餐,其他參數(shù)都是從OkHttpClient里面取到的靶瘸。
    Route:包含了Address(域名和端口)、Proxy代理和IP地址毛肋。合并連接中需要Route中結合Proxy和IP地址端口決定的奕锌,所以傳了Route才能找到連接路線才能進行連接合并。
    同樣拿到之后也是會返回result = transmitter.connection
  • 7:假如第二次還是沒取到村生,foundPooledConnection = false,那就自己創(chuàng)建一個連接饼丘。
  • 8:建立一個連接趁桃。(7只是創(chuàng)建)
  • 9:為什么還要再去連接池里面找呢?當連接池里面沒有對應的可用連接時,第一次和第二次肯定找不到卫病,那么就要自己創(chuàng)建連接了油啤,此時剛好有兩個請求都是指向同一個連接,那么創(chuàng)建兩個相同的連接就浪費資源了蟀苛。所以先創(chuàng)建好的去連接池里面找找不到益咬,那么就到了10(直接把連接塞進連接池);之后因為synchronized帜平,后面進來的去連接池里面找幽告,發(fā)現(xiàn)找到了,就把剛才自己創(chuàng)建好的連接扔掉裆甩。這里創(chuàng)建的連接都是都是多路復用的冗锁,因為剛才請求都是同個路徑,所以需要多路復用嗤栓。
  • 10:把新建的連接放進連接池冻河。
    剛才說初次進來的時候connection肯定是空的,但是現(xiàn)在創(chuàng)建完就不為空茉帅,那么這里是什么時候調(diào)用的叨叙?當請求失敗,重定向的時候堪澎。
  • 1:當我重新回來的時候擂错,發(fā)現(xiàn)有新的連接,并且不能重新復用的全封,那就把這個連接設為關閉马昙,到后面再把連接斷開。
  • 2:經(jīng)過1之后刹悴,說明這連接時可以用的行楞,那就直接拿這連接來使用。

總結一下:去找可用連接有五個操作
1土匀、看看有沒有可用連接子房,有可用連接且符合就直接用。
2就轧、從池子里面拿一個不帶多路復用的連接证杭。
3、從池子里面拿一個可以多路復用的連接妒御。
4解愤、創(chuàng)建連接。
5乎莉、創(chuàng)建過連接送讲,去池子里面拿奸笤,只拿多路復用的連接,拿到的話就把剛才創(chuàng)建的扔掉哼鬓。

注意:我們的目的是要得到一個編碼解碼器codec才引申出健康的連接再到怎么取連接监右。

現(xiàn)在來看下什么是這個取到的連接怎么才是健康的

  //RealConnection.kt
  /** Returns true if this connection is ready to host new streams. */
  fun isHealthy(doExtensiveChecks: Boolean): Boolean {
    val socket = this.socket!!
    val source = this.source!!
    if (socket.isClosed || socket.isInputShutdown || socket.isOutputShutdown) {
      return false
    }

    val http2Connection = this.http2Connection
    if (http2Connection != null) {
      return http2Connection.isHealthy(System.nanoTime())
    }

    if (doExtensiveChecks) {
      try {
        val readTimeout = socket.soTimeout
        try {
          socket.soTimeout = 1
          return !source.exhausted()
        } finally {
          socket.soTimeout = readTimeout
        }
      } catch (_: SocketTimeoutException) {
        // Read timed out; socket is good.
      } catch (_: IOException) {
        return false // Couldn't read; socket is closed.
      }
    }

    return true
  }

這里面主要看socket關閉沒有,再看看http2健不健康也就是http2的是否超出心跳時間异希。

所以到這里就已經(jīng)有了一個健康可用的連接健盒,用于創(chuàng)建一個對應的編碼解碼器codec

回到連接攔截器里面的調(diào)用Transmitter.newExchange,現(xiàn)在codec有了称簿,這個codec不僅包含了編碼解碼器扣癣,還有一個可用的健康的連接。再把這個codec裝進Exchange予跌,然后返回給連接攔截器搏色,最后連接攔截器把exchange傳給下一個攔截器。

請求攔截器CallServerInterceptor

接著最后一個攔截器券册,CallServerInterceptor請求攔截器

  //CallServerInterceptor.kt
  @Throws(IOException::class)
  override fun intercept(chain: Interceptor.Chain): Response {
    val realChain = chain as RealInterceptorChain
    val exchange = realChain.exchange()
    val request = realChain.request()
    val requestBody = request.body
    val sentRequestMillis = System.currentTimeMillis()

    exchange.writeRequestHeaders(request)

    var invokeStartEvent = true
    var responseBuilder: Response.Builder? = null
    if (HttpMethod.permitsRequestBody(request.method) && requestBody != null) {
      // If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100
      // Continue" response before transmitting the request body. If we don't get that, return
      // what we did get (such as a 4xx response) without ever transmitting the request body.
      if ("100-continue".equals(request.header("Expect"), ignoreCase = true)) {

        exchange.flushRequest()
        responseBuilder = exchange.readResponseHeaders(expectContinue = true)
        exchange.responseHeadersStart()
        invokeStartEvent = false
      }
      if (responseBuilder == null) {
        if (requestBody.isDuplex()) {
          // Prepare a duplex body so that the application can send a request body later.
          exchange.flushRequest()
          val bufferedRequestBody = exchange.createRequestBody(request, true).buffer()
          requestBody.writeTo(bufferedRequestBody)
        } else {
          // Write the request body if the "Expect: 100-continue" expectation was met.
          val bufferedRequestBody = exchange.createRequestBody(request, false).buffer()
          requestBody.writeTo(bufferedRequestBody)
          bufferedRequestBody.close()
        }
      } else {
        exchange.noRequestBody()
        if (!exchange.connection()!!.isMultiplexed) {
          // If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection
          // from being reused. Otherwise we're still obligated to transmit the request body to
          // leave the connection in a consistent state.
          exchange.noNewExchangesOnConnection()
        }
      }
    } else {
      exchange.noRequestBody()
    }

    if (requestBody == null || !requestBody.isDuplex()) {
      exchange.finishRequest()
    }
    if (responseBuilder == null) {
      responseBuilder = exchange.readResponseHeaders(expectContinue = false)!!
      if (invokeStartEvent) {
        exchange.responseHeadersStart()
        invokeStartEvent = false
      }
    }
    var response = responseBuilder
        .request(request)
        .handshake(exchange.connection()!!.handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build()
    var code = response.code
    if (code == 100) {
      // Server sent a 100-continue even though we did not request one. Try again to read the actual
      // response status.
      responseBuilder = exchange.readResponseHeaders(expectContinue = false)!!
      if (invokeStartEvent) {
        exchange.responseHeadersStart()
      }
      response = responseBuilder
          .request(request)
          .handshake(exchange.connection()!!.handshake())
          .sentRequestAtMillis(sentRequestMillis)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build()
      code = response.code
    }

    exchange.responseHeadersEnd(response)

    response = if (forWebSocket && code == 101) {
      // Connection is upgrading, but we need to ensure interceptors see a non-null response body.
      response.newBuilder()
          .body(EMPTY_RESPONSE)
          .build()
    } else {
      response.newBuilder()
          .body(exchange.openResponseBody(response))
          .build()
    }
    if ("close".equals(response.request.header("Connection"), ignoreCase = true) ||
        "close".equals(response.header("Connection"), ignoreCase = true)) {
      exchange.noNewExchangesOnConnection()
    }
    if ((code == 204 || code == 205) && response.body?.contentLength() ?: -1L > 0L) {
      throw ProtocolException(
          "HTTP $code had non-zero Content-Length: ${response.body?.contentLength()}")
    }
    return response
  }

這個攔截器就比較簡單频轿,主要是去發(fā)請求和讀響應,然后把響應往回傳這么一個操作烁焙,因為責任鏈中最后一個點沒有前置和后置航邢,主要是把前面攔截器做的事情進行最后的請求和返回響應。這樣整個結果往回傳骄蝇,最終回到了RealCall里面的response = getResponseWithInterceptorChain()

攔截器流程

總結

整個OkHttp功能的實現(xiàn)就在這五個默認的攔截器中膳殷,所以先理解攔截器模式的工作機制是先決條件。這五個攔截器分別為: 重試攔截器九火、橋接攔截器赚窃、緩存攔截器、連接攔截器岔激、請求服務攔截器勒极。每一個攔截器負責的工作不一樣,就好像工廠流水線虑鼎,最終經(jīng)過這五道工序辱匿,就完成了最終的產(chǎn)品。但是與流水線不同的是,OkHttp中的攔截器每次發(fā)起請求都會在交給下一個攔截器之前干一些事情,在獲得了結果之后又干一些事情计维。整個過程在請求向是順序的,而響應向則是逆序晚顷。

當用戶發(fā)起一個請求后,會由任務分發(fā)起 Dispatcher 將請求包裝并交給重試攔截器處理。

1、重試攔截器在交出(交給下一個攔截器)之前邑贴,負責判斷用戶是否取消了請求限府;在獲得了結果之后,會根據(jù)響應碼判斷是否需要重定向痢缎,如果滿足條件那么就會重啟執(zhí)行所有攔截器。

2世澜、橋接攔截器在交出之前独旷,負責將HTTP協(xié)議必備的請求頭加入其中(如:Host)并添加一些默認的行為(如:GZIP壓縮);在獲得了結果后寥裂,調(diào)用保存cookie接口并解析GZIP數(shù)據(jù)嵌洼。

3、緩存攔截器顧名思義封恰,交出之前讀取并判斷是否使用緩存麻养;獲得結果后判斷是否緩存。

4诺舔、連接攔截器在交出之前鳖昌,負責找到或者新建一個連接,并獲得對應的socket流低飒;在獲得結果后不進行額外的處理许昨。

5、請求服務器攔截器進行真正的與服務器的通信褥赊,向服務器發(fā)送數(shù)據(jù)糕档,解析讀取的響應數(shù)據(jù)。

在經(jīng)過了這一系列的流程后拌喉,就完成了一次HTTP請求速那!

OkHttp的優(yōu)點:

  • 支持Http1、Http2
  • 連接池復用底層TCP尿背,減少請求延時
  • 無縫的支持GZIP減少數(shù)據(jù)流量端仰,提高傳輸效率
  • 緩存響應數(shù)據(jù)減少重復的網(wǎng)絡請求
  • 請求失敗自動重試,自動重定向
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末残家,一起剝皮案震驚了整個濱河市榆俺,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌坞淮,老刑警劉巖茴晋,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異回窘,居然都是意外死亡诺擅,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門啡直,熙熙樓的掌柜王于貴愁眉苦臉地迎上來烁涌,“玉大人苍碟,你說我怎么就攤上這事〈橹矗” “怎么了微峰?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長抒钱。 經(jīng)常有香客問我蜓肆,道長,這世上最難降的妖魔是什么谋币? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任仗扬,我火速辦了婚禮,結果婚禮上蕾额,老公的妹妹穿的比我還像新娘早芭。我一直安慰自己,他們只是感情好诅蝶,可當我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布退个。 她就那樣靜靜地躺著,像睡著了一般秤涩。 火紅的嫁衣襯著肌膚如雪帜乞。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天筐眷,我揣著相機與錄音黎烈,去河邊找鬼。 笑死匀谣,一個胖子當著我的面吹牛照棋,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播武翎,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼烈炭,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了宝恶?” 一聲冷哼從身側響起符隙,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎垫毙,沒想到半個月后霹疫,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡综芥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年丽蝎,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片膀藐。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡屠阻,死狀恐怖红省,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情国觉,我是刑警寧澤吧恃,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站麻诀,受9級特大地震影響蚜枢,放射性物質發(fā)生泄漏。R本人自食惡果不足惜针饥,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望需频。 院中可真熱鬧丁眼,春花似錦、人聲如沸昭殉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽挪丢。三九已至蹂风,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間乾蓬,已是汗流浹背惠啄。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留任内,地道東北人撵渡。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像死嗦,于是被迫代替她去往敵國和親趋距。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容