OKhttp攔截器詳解1—RetryAndFollowUpInterceptor幢泼、BridgeInterceptor、CacheInterceptor

前言

在上一篇文章中劲腿,我們梳理了一遍Okhttp的源碼旭绒,初步了解了基本執(zhí)行流程,是一篇總覽全局的文章焦人。拋磚引玉挥吵,通過上一篇文章我們搞明白了Okhttp的基本工作原理,也知道Okhttp網(wǎng)絡(luò)請(qǐng)求的真正執(zhí)行是通過攔截器鏈關(guān)聯(lián)的給個(gè)攔截器進(jìn)行處理的花椭,每個(gè)攔截器負(fù)責(zé)不同的功能伐债,下面我們就來一一分析每個(gè)攔截器。

攔截器責(zé)任鏈

OKHttp最核心的工作是在getResponseWithInterceptorChain()中進(jìn)行的媚创,在分析之前蹄殃,我們先來了解什么事責(zé)任鏈模式。

責(zé)任鏈模式

責(zé)任鏈顧名思義就是由一系列的負(fù)責(zé)者構(gòu)成的一個(gè)鏈條袋倔。

為請(qǐng)求創(chuàng)建了一個(gè)接受者對(duì)象的鏈雕蔽。這種模式給予請(qǐng)求的類型,對(duì)請(qǐng)求的發(fā)送者和接收者進(jìn)行解耦宾娜。在這種模式中批狐,通常每個(gè)接收者都包含對(duì)另一個(gè)接收者的引用。如果一個(gè)對(duì)象不能處理該請(qǐng)求前塔,那么它會(huì)吧相同的請(qǐng)求傳給下一個(gè)接收者嚣艇,以此類推。

攔截器流程

先看下RealCallgetResponseWithInterceptorChain()华弓,攔截器的添加和連接器鏈的執(zhí)行都在此方法中食零。

 @Throws(IOException::class)
  internal fun getResponseWithInterceptorChain(): Response {
    // 創(chuàng)建一系列攔截器
    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(
      call = this,
      interceptors = interceptors,
      index = 0,
      exchange = null,
      request = originalRequest,
      connectTimeoutMillis = client.connectTimeoutMillis,
      readTimeoutMillis = client.readTimeoutMillis,
      writeTimeoutMillis = client.writeTimeoutMillis
    )
     ...
      //攔截器鏈的執(zhí)行
      val response = chain.proceed(originalRequest)
     ...
      return response
     ...
  }

getResponseWithInterceptorChain()中攔截器鏈流程:

Okhttp責(zé)任鏈流程.jpg

回顧下上一篇介紹的Okhttp五大默認(rèn)攔截器:

  1. RetryAndFollowUpInterceptor第一個(gè)接觸到請(qǐng)求,最后接觸到響應(yīng)(U型調(diào)用)的默認(rèn)攔截器寂屏;負(fù)責(zé)處理錯(cuò)誤重試和重定向 贰谣。
  2. BridgeInterceptor:補(bǔ)全請(qǐng)求娜搂,并對(duì)響應(yīng)進(jìn)行額外處理。
  3. CacheInterceptor:請(qǐng)求前查詢緩存冈爹,獲得響應(yīng)并判斷是否需要緩存涌攻。
  4. ConnectInterceptor:與服務(wù)器完成TCP連接。
  5. CallServerInterceptor: 與服務(wù)器通信频伤;封裝請(qǐng)求數(shù)據(jù)與解析響應(yīng)數(shù)據(jù)(如:HTTP報(bào)文)恳谎。

RetryAndFollowUpInterceptor(重試、重定向)

如果沒有添加應(yīng)用攔截器憋肖,那么第一個(gè)攔截器就是RetryAndFollowUpInterceptor因痛,主要作用就是:連接失敗后進(jìn)行重試、對(duì)請(qǐng)求結(jié)果跟進(jìn)后進(jìn)行重定向岸更。接著看下它的Intercept方法:

@Throws(IOException::class)
  override fun intercept(chain: Interceptor.Chain): Response {
    val realChain = chain as RealInterceptorChain
    var request = chain.request
    val call = realChain.call
    var followUpCount = 0
    var priorResponse: Response? = null
    var newRoutePlanner = true
    var recoveredFailures = listOf<IOException>()
    while (true) {
      //準(zhǔn)備連接
      call.enterNetworkInterceptorExchange(request, newRoutePlanner, chain)

      var response: Response
      var closeActiveExchange = true
      try {
        if (call.isCanceled()) {
          throw IOException("Canceled")
        }

        try {
          //繼續(xù)執(zhí)行下一個(gè)Interceptor
          response = realChain.proceed(request)
          newRoutePlanner = true
        } catch (e: IOException) {
          // 嘗試與服務(wù)器通信失敗鸵膏。請(qǐng)求可能已經(jīng)發(fā)送
          if (!recover(e, call, request, requestSendStarted = e !is ConnectionShutdownException)) {
            throw e.withSuppressed(recoveredFailures)
          } else {
            recoveredFailures += e
          }
          newRoutePlanner = false
          continue
        }

        // 清除下游攔截器的其他請(qǐng)求標(biāo)頭、cookie 等
        response = response.newBuilder()
          .request(request)
          .priorResponse(priorResponse?.stripBody())
          .build()

        val exchange = call.interceptorScopedExchange
         //跟進(jìn)結(jié)果怎炊,主要作用是根據(jù)響應(yīng)碼處理請(qǐng)求谭企,返回Request不為空時(shí)則進(jìn)行重定向處理-拿到重定向的request
        val followUp = followUpRequest(response, exchange)
        
       //followUp為空,不需要重試评肆,直接返回
        if (followUp == null) {
          if (exchange != null && exchange.isDuplex) {
            call.timeoutEarlyExit()
          }
          closeActiveExchange = false
          return response
        }

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

        response.body.closeQuietly()
       //最多重試20次
        if (++followUpCount > MAX_FOLLOW_UPS) {
          throw ProtocolException("Too many follow-up requests: $followUpCount")
        }
       //賦值债查,以便再次請(qǐng)求
        request = followUp
        priorResponse = response
      } finally {
       // 請(qǐng)求沒成功,釋放資源瓜挽。
        call.exitNetworkInterceptorExchange(closeActiveExchange)
      }
    }
  }

重試

可以看到請(qǐng)求階段發(fā)生了IOException(老版本還會(huì)捕獲RouteException)盹廷,會(huì)通過recover方法判斷是否能夠進(jìn)行重試,若返回true久橙,則表示允許重試俄占。

private fun recover(
    e: IOException,
    call: RealCall,
    userRequest: Request,
    requestSendStarted: Boolean
  ): Boolean {
    // 應(yīng)用層禁止重試,就不重試
    if (!client.retryOnConnectionFailure) return false

    // 不能再次發(fā)送請(qǐng)求淆衷,就不重試(requestSendStarted只在http2的io異常中為true缸榄,先不管http2)
    if (requestSendStarted && requestIsOneShot(e, userRequest)) return false

    // 發(fā)生的異常是致命的,就不重試
    if (!isRecoverable(e, requestSendStarted)) return false

    // 沒有路由可以嘗試祝拯,就不重試
    if (!call.retryAfterFailure()) return false

    return true
  }

接下來看一下isRecoverable中判斷了哪些異常甚带。

private fun isRecoverable(e: IOException, requestSendStarted: Boolean): Boolean {
    // 出現(xiàn)協(xié)議異常,不能重試
    if (e is ProtocolException) {
      return false
    }

    // 如果發(fā)生中斷( requestSendStarted認(rèn)為它一直為false(不管http2))鹿驼,不要恢復(fù)欲低;但如果連接到路由器超時(shí)(socket超時(shí))辕宏,判定可以重試
    if (e is InterruptedIOException) {
      return e is SocketTimeoutException && !requestSendStarted
    }

    //SSL握手異常中畜晰,證書出現(xiàn)問題,不能重試
    if (e is SSLHandshakeException) {
      if (e.cause is CertificateException) {
        return false
      }
    }
  //SSL握手未授權(quán)異常 不能重試
    if (e is SSLPeerUnverifiedException) {
      return false
    }
    return true
  }
  1. 協(xié)議異常:例如你的請(qǐng)求或者服務(wù)器的響應(yīng)本身就存在問題瑞筐,沒有按照http協(xié)議來定義數(shù)據(jù)凄鼻,直接無法重試。
  2. 超時(shí)異常:可能由于網(wǎng)絡(luò)波動(dòng)造成了Socket管道的超時(shí),如果有下一個(gè)路由的話块蚌,嘗試下一個(gè)路由闰非。
  3. SSL證書異常/SSL驗(yàn)證失敗異常:證書驗(yàn)證失敗或者缺少證書,無法重試峭范。

簡(jiǎn)單總結(jié)一下财松,首先在用戶未禁止重試的前提下,如果出現(xiàn)了某些異常纱控,則會(huì)在isRecoverable中進(jìn)行判斷辆毡,進(jìn)過上面一系列判斷后,如果允許進(jìn)行重試甜害,就會(huì)再檢查當(dāng)前是否有可用路由進(jìn)行連接重試舶掖。比如DNS對(duì)域名解析后可能會(huì)返回多個(gè)IP,在一個(gè)IP失敗后尔店,嘗試另一個(gè)IP進(jìn)行重試眨攘。

重定向

如果realChain.proceed沒有發(fā)生異常,返回了結(jié)果response嚣州,就會(huì)使用followUpRequest方法跟進(jìn)結(jié)果并構(gòu)建重定向request鲫售。 如果不用跟進(jìn)處理(例如響應(yīng)碼是200),則返回null避诽」昊ⅲ看下followUpRequest方法:

  @Throws(IOException::class)
  private fun followUpRequest(userResponse: Response, exchange: Exchange?): Request? {
    val route = exchange?.connection?.route()
    val responseCode = userResponse.code
    val method = userResponse.request.method
    when (responseCode) {
       // 407 客戶端使用了HTTP代理服務(wù)器,在請(qǐng)求頭中添加 “Proxy-Authorization”沙庐,讓代理服務(wù)器授權(quán)
      HTTP_PROXY_AUTH -> {
        val selectedProxy = route!!.proxy
        if (selectedProxy.type() != Proxy.Type.HTTP) {
          throw ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy")
        }
        return client.proxyAuthenticator.authenticate(route, userResponse)
      }
       // 401 需要身份驗(yàn)證 有些服務(wù)器接口需要驗(yàn)證使用者身份 在請(qǐng)求頭中添加 “Authorization” 
      HTTP_UNAUTHORIZED -> return client.authenticator.authenticate(route, userResponse)
      //308 永久重定向 ,307 臨時(shí)重定向,300,301,302,303 
      HTTP_PERM_REDIRECT, HTTP_TEMP_REDIRECT, HTTP_MULT_CHOICE, HTTP_MOVED_PERM, HTTP_MOVED_TEMP, HTTP_SEE_OTHER -> {
        return buildRedirectRequest(userResponse, method)
      }
       // 408 客戶端請(qǐng)求超時(shí) 
      HTTP_CLIENT_TIMEOUT -> {
           // 408 算是連接失敗了鲤妥,所以判斷用戶是不是允許重試
        if (!client.retryOnConnectionFailure) {
          return null
        }
        val requestBody = userResponse.request.body
        if (requestBody != null && requestBody.isOneShot()) {
          return null
        }
        val priorResponse = userResponse.priorResponse
         // 如果是本身這次的響應(yīng)就是重新請(qǐng)求的產(chǎn)物同時(shí)上一次之所以重請(qǐng)求還是因?yàn)?08,那我們這次不再重請(qǐng)求了
        if (priorResponse != null && priorResponse.code == HTTP_CLIENT_TIMEOUT) {
          return null
        }
        
         // 如果服務(wù)器告訴我們了 Retry-After 多久后重試拱雏,那框架不管了棉安。
        if (retryAfter(userResponse, 0) > 0) {
          return null
        }
        return userResponse.request
      }
         // 503 服務(wù)不可用 和408差不多,但是只在服務(wù)器告訴你 Retry-After:0(意思就是立即重試) 才重請(qǐng)求
      HTTP_UNAVAILABLE -> {
        val priorResponse = userResponse.priorResponse
        if (priorResponse != null && priorResponse.code == HTTP_UNAVAILABLE) {
          return null
        }
        if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
          return userResponse.request
        }
        return null
      }
      HTTP_MISDIRECTED_REQUEST -> {
        val requestBody = userResponse.request.body
        if (requestBody != null && requestBody.isOneShot()) {
          return null
        }
        if (exchange == null || !exchange.isCoalescedConnection) {
          return null
        }
        exchange.connection.noCoalescedConnections()
        return userResponse.request
      }

      else -> return null
    }
  }

主要就是根據(jù)響應(yīng)碼判斷如果需要重定向铸抑,如果此方法返回空贡耽,那就表示不需要再重定向了,直接返回響應(yīng)鹊汛;但是如果返回非空蒲赂,那就要重新請(qǐng)求返回的Request,但是需要注意的是刁憋,followup`在攔截器中定義的最大次數(shù)為20次滥嘴。

總結(jié)

  1. 如果沒有添加應(yīng)用攔截器,那么RetryAndFollowUpInterceptor就是第一個(gè)攔截器至耻,主要功能是判斷是否需要重試和重定向若皱。
  2. 如果請(qǐng)求階段發(fā)生了IOException镊叁,就會(huì)通過recover方法判斷是進(jìn)行連接重試。
  3. 重定向發(fā)生在重試的判定之后走触,如果不滿足重試的條件晦譬,還需要進(jìn)一步調(diào)用followUpRequest根據(jù)Response 的響應(yīng)碼(當(dāng)然,如果直接請(qǐng)求失敗互广,Response都不存在就會(huì)拋出異常)判斷是否重定向敛腌;followup最大發(fā)生次數(shù)20次。

BridgeInterceptor(橋接攔截器)

BridgeInterceptor惫皱,連接應(yīng)用程序和服務(wù)器的橋梁迎瞧,我們發(fā)出的請(qǐng)求會(huì)經(jīng)過它的處理才能發(fā)給服務(wù)器,比如設(shè)置請(qǐng)求內(nèi)容的長度逸吵,編碼凶硅,gzip壓縮,cookie等扫皱;獲取響應(yīng)后保存cookie足绅、解壓等操作。相對(duì)比較簡(jiǎn)單韩脑。

首先氢妈,chain.proceed()執(zhí)行前,對(duì)請(qǐng)求頭進(jìn)行補(bǔ)全段多,補(bǔ)全請(qǐng)求頭如下:

請(qǐng)求頭 說明
Content-Type 請(qǐng)求體類型,如:application/x-www-form-urlencoded
Content-Length/Transfer-Encoding 請(qǐng)求體解析方式
Host 請(qǐng)求的主機(jī)站點(diǎn)
Connection: Keep-Alive 保持長連接
Accept-Encoding: gzip 接受響應(yīng)支持gzip壓縮
Cookie cookie身份辨別
User-Agent 請(qǐng)求的用戶信息首量,如:操作系統(tǒng)、瀏覽器等

在補(bǔ)全了請(qǐng)求頭后交給下一個(gè)攔截器處理进苍,得到響應(yīng)后加缘,主要干兩件事情:

  1. 先把響應(yīng)header中的cookie存入cookieJar(如果有),在下次請(qǐng)求則會(huì)讀取對(duì)應(yīng)的數(shù)據(jù)設(shè)置進(jìn)入請(qǐng)求頭觉啊,默認(rèn)的CookieJar不提供實(shí)現(xiàn)拣宏,需要我們?cè)诔跏蓟疧khttpClient時(shí)配置我們自己的cookieJar。
  2. 如果使用gzip返回的數(shù)據(jù)杠人,則使用GzipSource包裝便于解析勋乾。

總結(jié)

  1. 對(duì)用戶構(gòu)建的Request進(jìn)行添加或者刪除相關(guān)頭部信息,以轉(zhuǎn)化成能夠真正進(jìn)行網(wǎng)絡(luò)請(qǐng)求的Request嗡善。
  2. 將符合網(wǎng)絡(luò)請(qǐng)求規(guī)范的Request交給下一個(gè)攔截器處理辑莫,并獲取Response
  3. 如果響應(yīng)體經(jīng)過了gzip壓縮罩引,那就需要解壓各吨,再構(gòu)建成用戶可用的Response并返回。

CacheInterceptor(緩存攔截器)

CacheInterceptor,在發(fā)出請(qǐng)求前蜒程,先判斷是否命中緩存绅你,如果命中則可以不請(qǐng)求,直接使用緩存的響應(yīng)(默認(rèn)只會(huì)對(duì)Get請(qǐng)求進(jìn)行緩存)昭躺;如果未命中則進(jìn)行網(wǎng)絡(luò)請(qǐng)求忌锯,并將結(jié)果緩存,等待下次請(qǐng)求被命中领炫。

先來看下CacheInterceptorintercept方法代碼:

 @Throws(IOException::class)
  override fun intercept(chain: Interceptor.Chain): Response {
    val call = chain.call()
    // 用request的url 從緩存中獲取Response作為候選(CacheStrategy決定是否使用)
    val cacheCandidate = cache?.get(chain.request())

    val now = System.currentTimeMillis()
    // 根據(jù)request偶垮、候選Response 獲取緩存策略。
    // 緩存策略 將決定是否使用緩存:strategy.networkRequest為null帝洪,不適用網(wǎng)絡(luò)似舵;
    // strategy.cacheResponse為null,不使用緩存
    val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()
    val networkRequest = strategy.networkRequest
    val cacheResponse = strategy.cacheResponse
   // 根據(jù)緩存策略更新統(tǒng)計(jì)指標(biāo):請(qǐng)求次數(shù)葱峡、網(wǎng)絡(luò)請(qǐng)求次數(shù)砚哗、使用緩存次數(shù)
    cache?.trackResponse(strategy)
    val listener = (call as? RealCall)?.eventListener ?: EventListener.NONE

    if (cacheCandidate != null && cacheResponse == null) {
      // The cache candidate wasn't applicable. Close it.
      cacheCandidate.body.closeQuietly()
    }

    // 網(wǎng)絡(luò)請(qǐng)求、緩存 都不能用砰奕,就返回504
    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)")
          .sentRequestAtMillis(-1L)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build().also {
            listener.satisfactionFailure(call, it)
          }
    }

    // 如果不用網(wǎng)絡(luò)蛛芥,cacheResponse肯定不為空了,那么就使用緩存了军援,結(jié)束了
    if (networkRequest == null) {
      return cacheResponse!!.newBuilder()
          .cacheResponse(cacheResponse.stripBody())
          .build().also {
            listener.cacheHit(call, it)
          }
    }

    if (cacheResponse != null) {
      listener.cacheConditionalHit(call, cacheResponse)
    } else if (cache != null) {
      listener.cacheMiss(call)
    }
   //到這里仅淑,networkRequest != null (cacheResponse可能為null,也可能!null)
   //networkRequest != null,就要進(jìn)行網(wǎng)絡(luò)請(qǐng)求了胸哥, 所以攔截器鏈就繼續(xù)往下處理了
    var networkResponse: Response? = null
    try {
      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()
      }
    }

    // 如果網(wǎng)絡(luò)請(qǐng)求返回304涯竟,表示服務(wù)端資源沒有修改,那么就結(jié)合網(wǎng)絡(luò)響應(yīng)和緩存響應(yīng)空厌,然后更新緩存->返回->結(jié)束
    if (cacheResponse != null) {
      if (networkResponse?.code == HTTP_NOT_MODIFIED) {
        val response = cacheResponse.newBuilder()
            .headers(combine(cacheResponse.headers, networkResponse.headers))//結(jié)合header
            .sentRequestAtMillis(networkResponse.sentRequestAtMillis)//請(qǐng)求事件
            .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis)//接受事件
            .cacheResponse(cacheResponse.stripBody())
            .networkResponse(networkResponse.stripBody())
            .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.also {
          listener.cacheHit(call, it)
        }
      } else {
        //如果是非304庐船,說明服務(wù)端資源有更新,就關(guān)閉緩存body
        cacheResponse.body.closeQuietly()
      }
    }

    val response = networkResponse!!.newBuilder()
        .cacheResponse(cacheResponse?.stripBody())
        .networkResponse(networkResponse.stripBody())
        .build()

    if (cache != null) {
      //網(wǎng)絡(luò)響應(yīng)可緩存(請(qǐng)求和響應(yīng)的頭Cache-Control都不是'no-store')
      if (response.promisesBody() && CacheStrategy.isCacheable(response, networkRequest)) {
        // 寫入緩存
        val cacheRequest = cache.put(response)
        return cacheWritingResponse(cacheRequest, response).also {
          if (cacheResponse != null) {
            // This will log a conditional cache miss only.
            listener.cacheMiss(call)
          }
        }
      }
//Okhttp默認(rèn)只會(huì)對(duì)get請(qǐng)求進(jìn)行緩存嘲更,因?yàn)間et請(qǐng)求的數(shù)據(jù)一般是比較持久的醉鳖,而post一般是交互操作,沒太大意義進(jìn)行緩存
   //不是get請(qǐng)求就移除緩存
      if (HttpMethod.invalidatesCache(networkRequest.method)) {
        try {
          cache.remove(networkRequest)
        } catch (_: IOException) {
          // The cache cannot be written.
        }
      }
    }
    return response
  }

大概梳理一下步驟:

  1. 從緩存中獲取對(duì)應(yīng)請(qǐng)求的響應(yīng)緩存

  2. 創(chuàng)建CacheStrategy哮内,創(chuàng)建時(shí)會(huì)判斷是否能夠使用緩存盗棵,在CacheStrategy中有兩個(gè)成員:networkRequestcacheResponse。他們有如下組合進(jìn)行判斷:

    networkRequest cacheResponse 說明
    Null Null 網(wǎng)絡(luò)請(qǐng)求北发、緩存 都不能用纹因,okhttp直接返回504
    Null Not Null 直接使用緩存
    Not Null Null 向服務(wù)器發(fā)起請(qǐng)求
    Not Null Not Null 向服務(wù)器發(fā)起請(qǐng)求,若得到響應(yīng)碼為304(無修改)琳拨,則更新緩存響應(yīng)并返回
  3. 交給下一個(gè)責(zé)任鏈繼續(xù)處理

  4. 后續(xù)工作瞭恰,返回304則用緩存的響應(yīng);否則使用網(wǎng)絡(luò)響應(yīng)并緩存本次響應(yīng)(只緩存Get請(qǐng)求的響應(yīng))

緩存攔截器中判斷使用緩存或者請(qǐng)求服務(wù)器都是通過CacheStrategy判斷狱庇。分析CacheStrategy前我們先來了解下Http的緩存機(jī)制惊畏,以便更好的理解:

首次請(qǐng)求:

1726b5f779033daf_tplv-t2oaga2asx-zoom-in-crop-mark_4536_0_0_0.jpg

第二次請(qǐng)求:
1726b5f779755ba1_tplv-t2oaga2asx-zoom-in-crop-mark_4536_0_0_0.jpg

上面兩張圖是對(duì)http緩存機(jī)制的一個(gè)總結(jié):根據(jù) 緩存是否過期恶耽、過期后是否有修改 來決定 請(qǐng)求是否使用緩存。詳細(xì)內(nèi)容可看這篇文章: 徹底弄懂HTTP緩存機(jī)制及原理颜启。

回過頭我們?cè)賮砜?code>CacheStrategy偷俭,它的生成代碼看起來很簡(jiǎn)單,只有一行代碼:

 val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()

把請(qǐng)求request缰盏、獲選緩存cacheCandidate傳入工廠類Factory涌萤,然后調(diào)用compute()方法。

  class Factory(
    private val nowMillis: Long,
    internal val request: Request,
    private val cacheResponse: Response?
  ) {
    init {
      if (cacheResponse != null) {
        /*獲取候選緩存的請(qǐng)求時(shí)間口猜、響應(yīng)時(shí)間负溪,從header中獲取 過期時(shí)間、修改時(shí)間济炎、資源標(biāo)記等(如果有)*/
        this.sentRequestMillis = cacheResponse.sentRequestAtMillis
        this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis
        val headers = cacheResponse.headers
        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)
            }
          }
        }
      }
    }
    
     /** Returns a strategy to satisfy [request] using [cacheResponse]. */
    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
    }
  }

Factory構(gòu)造方法內(nèi)川抡,獲取了候選響應(yīng)的請(qǐng)求時(shí)間、響應(yīng)時(shí)間须尚、過期時(shí)長猖腕、修改時(shí)間、資源標(biāo)記等恨闪。

compute方法內(nèi)部先調(diào)用了computeCandidate()獲取到緩存策略實(shí)例倘感,先跟進(jìn)到computeCandidate方法中:

/** Returns a strategy to use assuming the request can use the network. */
    private fun computeCandidate(): CacheStrategy {
      // 沒有緩存——進(jìn)行網(wǎng)絡(luò)請(qǐng)求
      if (cacheResponse == null) {
        return CacheStrategy(request, null)
      }
      //https請(qǐng)求,但沒有握手——進(jìn)行網(wǎng)絡(luò)請(qǐng)求
      if (request.isHttps && cacheResponse.handshake == null) {
        return CacheStrategy(request, null)
      }

      // 網(wǎng)絡(luò)響應(yīng)不可緩存(請(qǐng)求或響應(yīng)頭Cache-Control是'no-store')——進(jìn)行網(wǎng)絡(luò)請(qǐng)求
      if (!isCacheable(cacheResponse, request)) {
        return CacheStrategy(request, null)
      }
   //請(qǐng)求頭Cache-Control是no-cache或者請(qǐng)求頭有"If-Modified-Since"或"If-None-Match"——進(jìn)行網(wǎng)絡(luò)請(qǐng)求
    //意思是不使用緩存或者請(qǐng)求手動(dòng)添加了頭部"If-Modified-Since"或"If-None-Match"
      val requestCaching = request.cacheControl
      if (requestCaching.noCache || hasConditions(request)) {
        return CacheStrategy(request, null)
      }
   
      val responseCaching = cacheResponse.cacheControl
     //緩存的年齡
      val ageMillis = cacheResponseAge()
      //緩存的有效期
      var freshMillis = computeFreshnessLifetime()
     //比較請(qǐng)求頭里有效期咙咽,取較小值
      if (requestCaching.maxAgeSeconds != -1) {
        freshMillis = minOf(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds.toLong()))
      }
      //可接受緩存的最小剩余時(shí)間(min-fresh表示客戶端不愿意接收剩余有效期<=min-fresh的緩存)
      var minFreshMillis: Long = 0
      if (requestCaching.minFreshSeconds != -1) {
        minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds.toLong())
      }
      //可接受緩存的最大過期時(shí)間(max-stale指令表示了客戶端愿意接收一個(gè)過期了的緩存老玛,例如:過期了1小時(shí)還可以用)
      var maxStaleMillis: Long = 0
      // 第一個(gè)判斷:是否要求必須去服務(wù)器驗(yàn)證資源狀態(tài)
      // 第二個(gè)判斷:獲取max-stale值,如果不等于-1钧敞,說明緩存過期后還能使用指定的時(shí)長
      if (!responseCaching.mustRevalidate && requestCaching.maxStaleSeconds != -1) {
        maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds.toLong())
      }
//如果響應(yīng)頭沒有要求忽略本地緩存并且整合后的緩存年齡小于整合后的過期時(shí)間蜡豹,那么緩存就可以用
      if (!responseCaching.noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
        val builder = cacheResponse.newBuilder()
          //沒有滿足“可接受的最小剩余有效時(shí)間”,加個(gè)110警告
        if (ageMillis + minFreshMillis >= freshMillis) {
          builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"")
        }
        val oneDayMillis = 24 * 60 * 60 * 1000L
        //isFreshnessLifetimeHeuristic表示沒有過期時(shí)間且緩存的年齡大于一天溉苛,就加個(gè)113警告
        if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
          builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"")
        }
        return CacheStrategy(null, builder.build())
      }

      //到這里镜廉,說明緩存是過期的
      //然后找緩存里的Etag、lastModified愚战、servedDate
      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
        }
        //都沒有娇唯,就執(zhí)行常規(guī)網(wǎng)絡(luò)請(qǐng)求
        else -> return CacheStrategy(request, null) // No condition! Make a regular request.
      }
   // 如果有,就添加到網(wǎng)絡(luò)請(qǐng)求的頭部
      val conditionalRequestHeaders = request.headers.newBuilder()
      conditionalRequestHeaders.addLenient(conditionName, conditionValue!!)

      val conditionalRequest = request.newBuilder()
          .headers(conditionalRequestHeaders.build())
          .build()
      //conditionalRequest(條件網(wǎng)絡(luò)請(qǐng)求):有緩存但過期了寂玲,去請(qǐng)求網(wǎng)絡(luò)詢問服務(wù)器是否能用塔插,能用側(cè)返回304,不能則正常執(zhí)行網(wǎng)絡(luò)請(qǐng)求
      return CacheStrategy(conditionalRequest, cacheResponse)
    }

同樣經(jīng)過一系列判斷:

  1. 沒有緩存拓哟、https請(qǐng)求但沒有握手想许、手動(dòng)設(shè)置不緩存、忽略緩存或者手動(dòng)配置緩存過期,都是直接進(jìn)行網(wǎng)絡(luò)請(qǐng)求流纹。
  2. 1中都不滿足時(shí)糜烹,如果緩存沒有過期,則使用緩存(可能會(huì)添加警告)漱凝。
  3. 1中都不滿足時(shí)疮蹦,如果緩存過期了,但響應(yīng)頭有Etag碉哑,Last-Modified,Date亮蒋,就添加這些header進(jìn)行條件網(wǎng)絡(luò)請(qǐng)求扣典。
  4. 1中都不滿足時(shí),如果緩存過期了慎玖,且響應(yīng)頭沒有設(shè)置Etag贮尖,Last-Modified,Date趁怔,就進(jìn)行常規(guī)網(wǎng)絡(luò)請(qǐng)求湿硝。

回頭接著再看compute()方法:

  /** Returns a strategy to satisfy [request] using [cacheResponse]. */
    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
    }
  }

computeCandidate()獲取到緩存策略候選后,又進(jìn)行了一個(gè)判斷:使用網(wǎng)絡(luò)請(qǐng)求 但是 原請(qǐng)求配置了只能使用緩存润努,按照上面的分析关斜,此時(shí)即使有緩存,也是過期的緩存铺浇,所以又new了實(shí)例痢畜,兩個(gè)值都為null。

到這里okhttp的緩存機(jī)制源碼就看完了鳍侣。okhttp的緩存機(jī)制是符合開頭http的緩存機(jī)制那兩張圖的丁稀,只是增加很多細(xì)節(jié)判斷。

另外注意倚聚,緩存的讀寫是通過 InternalCache完成的线衫。InternalCache是在創(chuàng)建CacheInterceptor實(shí)例時(shí) 用client.internalCache()作為參數(shù)傳入。而InternalCache是okhttp內(nèi)部使用惑折,類似一個(gè)代理授账,InternalCache的實(shí)例是 類的屬性。Cache 是我們初始化 OkHttpClient時(shí)傳入的惨驶。所以如果沒有傳入Cache實(shí)例是沒有緩存功能的矗积。

 val client = OkHttpClient.Builder()
                .cache(Cache(getExternalCacheDir(),500 * 1024 * 1024))
                .build()

緩存的增刪改查,Cache是通過okhttp內(nèi)部的DiskLruCache實(shí)現(xiàn)的敞咧,原理和jakewharton的DiskLruCache是一致的棘捣,這里就不再敘述。

結(jié)語

本篇文章講解了三個(gè)攔截器工作原理,分別是:RetryAndFollowUpInterceptor乍恐、BridgeInterceptor评疗、CacheInterceptor。由于篇幅原因茵烈,剩下兩個(gè)攔截器:ConnectInterceptor百匆、CallServerInterceptor,放在下一篇進(jìn)行講解呜投。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末加匈,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子仑荐,更是在濱河造成了極大的恐慌雕拼,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,204評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件粘招,死亡現(xiàn)場(chǎng)離奇詭異啥寇,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)洒扎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門辑甜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人袍冷,你說我怎么就攤上這事磷醋。” “怎么了胡诗?”我有些...
    開封第一講書人閱讀 164,548評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵子檀,是天一觀的道長。 經(jīng)常有香客問我乃戈,道長褂痰,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,657評(píng)論 1 293
  • 正文 為了忘掉前任症虑,我火速辦了婚禮缩歪,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘谍憔。我一直安慰自己匪蝙,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,689評(píng)論 6 392
  • 文/花漫 我一把揭開白布习贫。 她就那樣靜靜地躺著逛球,像睡著了一般。 火紅的嫁衣襯著肌膚如雪苫昌。 梳的紋絲不亂的頭發(fā)上颤绕,一...
    開封第一講書人閱讀 51,554評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼奥务。 笑死物独,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的氯葬。 我是一名探鬼主播挡篓,決...
    沈念sama閱讀 40,302評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼帚称!你這毒婦竟也來了官研?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,216評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤闯睹,失蹤者是張志新(化名)和其女友劉穎戏羽,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體瞻坝,經(jīng)...
    沈念sama閱讀 45,661評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蛛壳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,851評(píng)論 3 336
  • 正文 我和宋清朗相戀三年杏瞻,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了所刀。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,977評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡捞挥,死狀恐怖浮创,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情砌函,我是刑警寧澤斩披,帶...
    沈念sama閱讀 35,697評(píng)論 5 347
  • 正文 年R本政府宣布,位于F島的核電站讹俊,受9級(jí)特大地震影響垦沉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜仍劈,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,306評(píng)論 3 330
  • 文/蒙蒙 一厕倍、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧贩疙,春花似錦讹弯、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至悲靴,卻和暖如春臭胜,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評(píng)論 1 270
  • 我被黑心中介騙來泰國打工庇楞, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留榜配,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,138評(píng)論 3 370
  • 正文 我出身青樓吕晌,卻偏偏與公主長得像蛋褥,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子睛驳,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,927評(píng)論 2 355

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