OKhttp拦截器详解1—RetryAndFollowUpInterceptor、BridgeInterceptor、CacheInterceptor

前言

在上一篇文章中,我们梳理了一遍Okhttp的源码,初步了解了基本执行流程,是一篇总览全局的文章。抛砖引玉,通过上一篇文章我们搞明白了Okhttp的基本工作原理,也知道Okhttp网络请求的真正执行是通过拦截器链关联的给个拦截器进行处理的,每个拦截器负责不同的功能,下面我们就来一一分析每个拦截器。

拦截器责任链

OKHttp最核心的工作是在getResponseWithInterceptorChain()中进行的,在分析之前,我们先来了解什么事责任链模式。

责任链模式

责任链顾名思义就是由一系列的负责者构成的一个链条。

为请求创建了一个接受者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。在这种模式中,通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会吧相同的请求传给下一个接收者,以此类推。

拦截器流程

先看下RealCallgetResponseWithInterceptorChain(),拦截器的添加和连接器链的执行都在此方法中。

 @Throws(IOException::class)
  internal fun getResponseWithInterceptorChain(): Response {
    // 创建一系列拦截器
    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
    )
     ...
      //拦截器链的执行
      val response = chain.proceed(originalRequest)
     ...
      return response
     ...
  }

getResponseWithInterceptorChain()中拦截器链流程:

Okhttp责任链流程.jpg

回顾下上一篇介绍的Okhttp五大默认拦截器:

  1. RetryAndFollowUpInterceptor第一个接触到请求,最后接触到响应(U型调用)的默认拦截器;负责处理错误重试和重定向 。
  2. BridgeInterceptor:补全请求,并对响应进行额外处理。
  3. CacheInterceptor:请求前查询缓存,获得响应并判断是否需要缓存。
  4. ConnectInterceptor:与服务器完成TCP连接。
  5. CallServerInterceptor: 与服务器通信;封装请求数据与解析响应数据(如:HTTP报文)。

RetryAndFollowUpInterceptor(重试、重定向)

如果没有添加应用拦截器,那么第一个拦截器就是RetryAndFollowUpInterceptor,主要作用就是:连接失败后进行重试、对请求结果跟进后进行重定向。接着看下它的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) {
      //准备连接
      call.enterNetworkInterceptorExchange(request, newRoutePlanner, chain)

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

        try {
          //继续执行下一个Interceptor
          response = realChain.proceed(request)
          newRoutePlanner = true
        } catch (e: IOException) {
          // 尝试与服务器通信失败。请求可能已经发送
          if (!recover(e, call, request, requestSendStarted = e !is ConnectionShutdownException)) {
            throw e.withSuppressed(recoveredFailures)
          } else {
            recoveredFailures += e
          }
          newRoutePlanner = false
          continue
        }

        // 清除下游拦截器的其他请求标头、cookie 等
        response = response.newBuilder()
          .request(request)
          .priorResponse(priorResponse?.stripBody())
          .build()

        val exchange = call.interceptorScopedExchange
         //跟进结果,主要作用是根据响应码处理请求,返回Request不为空时则进行重定向处理-拿到重定向的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")
        }
       //赋值,以便再次请求
        request = followUp
        priorResponse = response
      } finally {
       // 请求没成功,释放资源。
        call.exitNetworkInterceptorExchange(closeActiveExchange)
      }
    }
  }

重试

可以看到请求阶段发生了IOException(老版本还会捕获RouteException),会通过recover方法判断是否能够进行重试,若返回true,则表示允许重试。

private fun recover(
    e: IOException,
    call: RealCall,
    userRequest: Request,
    requestSendStarted: Boolean
  ): Boolean {
    // 应用层禁止重试,就不重试
    if (!client.retryOnConnectionFailure) return false

    // 不能再次发送请求,就不重试(requestSendStarted只在http2的io异常中为true,先不管http2)
    if (requestSendStarted && requestIsOneShot(e, userRequest)) return false

    // 发生的异常是致命的,就不重试
    if (!isRecoverable(e, requestSendStarted)) return false

    // 没有路由可以尝试,就不重试
    if (!call.retryAfterFailure()) return false

    return true
  }

接下来看一下isRecoverable中判断了哪些异常。

private fun isRecoverable(e: IOException, requestSendStarted: Boolean): Boolean {
    // 出现协议异常,不能重试
    if (e is ProtocolException) {
      return false
    }

    // 如果发生中断( requestSendStarted认为它一直为false(不管http2)),不要恢复;但如果连接到路由器超时(socket超时),判定可以重试
    if (e is InterruptedIOException) {
      return e is SocketTimeoutException && !requestSendStarted
    }

    //SSL握手异常中,证书出现问题,不能重试
    if (e is SSLHandshakeException) {
      if (e.cause is CertificateException) {
        return false
      }
    }
  //SSL握手未授权异常 不能重试
    if (e is SSLPeerUnverifiedException) {
      return false
    }
    return true
  }
  1. 协议异常:例如你的请求或者服务器的响应本身就存在问题,没有按照http协议来定义数据,直接无法重试。
  2. 超时异常:可能由于网络波动造成了Socket管道的超时,如果有下一个路由的话,尝试下一个路由。
  3. SSL证书异常/SSL验证失败异常:证书验证失败或者缺少证书,无法重试。

简单总结一下,首先在用户未禁止重试的前提下,如果出现了某些异常,则会在isRecoverable中进行判断,进过上面一系列判断后,如果允许进行重试,就会再检查当前是否有可用路由进行连接重试。比如DNS对域名解析后可能会返回多个IP,在一个IP失败后,尝试另一个IP进行重试。

重定向

如果realChain.proceed没有发生异常,返回了结果response,就会使用followUpRequest方法跟进结果并构建重定向request。 如果不用跟进处理(例如响应码是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代理服务器,在请求头中添加 “Proxy-Authorization”,让代理服务器授权
      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 需要身份验证 有些服务器接口需要验证使用者身份 在请求头中添加 “Authorization” 
      HTTP_UNAUTHORIZED -> return client.authenticator.authenticate(route, userResponse)
      //308 永久重定向 ,307 临时重定向,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 客户端请求超时 
      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
         // 如果是本身这次的响应就是重新请求的产物同时上一次之所以重请求还是因为408,那我们这次不再重请求了
        if (priorResponse != null && priorResponse.code == HTTP_CLIENT_TIMEOUT) {
          return null
        }
        
         // 如果服务器告诉我们了 Retry-After 多久后重试,那框架不管了。
        if (retryAfter(userResponse, 0) > 0) {
          return null
        }
        return userResponse.request
      }
         // 503 服务不可用 和408差不多,但是只在服务器告诉你 Retry-After:0(意思就是立即重试) 才重请求
      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
    }
  }

主要就是根据响应码判断如果需要重定向,如果此方法返回空,那就表示不需要再重定向了,直接返回响应;但是如果返回非空,那就要重新请求返回的Request,但是需要注意的是,followup`在拦截器中定义的最大次数为20次。

总结

  1. 如果没有添加应用拦截器,那么RetryAndFollowUpInterceptor就是第一个拦截器,主要功能是判断是否需要重试和重定向。
  2. 如果请求阶段发生了IOException,就会通过recover方法判断是进行连接重试。
  3. 重定向发生在重试的判定之后,如果不满足重试的条件,还需要进一步调用followUpRequest根据Response 的响应码(当然,如果直接请求失败,Response都不存在就会抛出异常)判断是否重定向;followup最大发生次数20次。

BridgeInterceptor(桥接拦截器)

BridgeInterceptor,连接应用程序和服务器的桥梁,我们发出的请求会经过它的处理才能发给服务器,比如设置请求内容的长度,编码,gzip压缩,cookie等;获取响应后保存cookie、解压等操作。相对比较简单。

首先,chain.proceed()执行前,对请求头进行补全,补全请求头如下:

请求头 说明
Content-Type 请求体类型,如:application/x-www-form-urlencoded
Content-Length/Transfer-Encoding 请求体解析方式
Host 请求的主机站点
Connection: Keep-Alive 保持长连接
Accept-Encoding: gzip 接受响应支持gzip压缩
Cookie cookie身份辨别
User-Agent 请求的用户信息,如:操作系统、浏览器等

在补全了请求头后交给下一个拦截器处理,得到响应后,主要干两件事情:

  1. 先把响应header中的cookie存入cookieJar(如果有),在下次请求则会读取对应的数据设置进入请求头,默认的CookieJar不提供实现,需要我们在初始化OkhttpClient时配置我们自己的cookieJar。
  2. 如果使用gzip返回的数据,则使用GzipSource包装便于解析。

总结

  1. 对用户构建的Request进行添加或者删除相关头部信息,以转化成能够真正进行网络请求的Request
  2. 将符合网络请求规范的Request交给下一个拦截器处理,并获取Response
  3. 如果响应体经过了gzip压缩,那就需要解压,再构建成用户可用的Response并返回。

CacheInterceptor(缓存拦截器)

CacheInterceptor,在发出请求前,先判断是否命中缓存,如果命中则可以不请求,直接使用缓存的响应(默认只会对Get请求进行缓存);如果未命中则进行网络请求,并将结果缓存,等待下次请求被命中。

先来看下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()
    // 根据request、候选Response 获取缓存策略。
    // 缓存策略 将决定是否使用缓存:strategy.networkRequest为null,不适用网络;
    // strategy.cacheResponse为null,不使用缓存
    val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()
    val networkRequest = strategy.networkRequest
    val cacheResponse = strategy.cacheResponse
   // 根据缓存策略更新统计指标:请求次数、网络请求次数、使用缓存次数
    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()
    }

    // 网络请求、缓存 都不能用,就返回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)
          }
    }

    // 如果不用网络,cacheResponse肯定不为空了,那么就使用缓存了,结束了
    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,就要进行网络请求了, 所以拦截器链就继续往下处理了
    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()
      }
    }

    // 如果网络请求返回304,表示服务端资源没有修改,那么就结合网络响应和缓存响应,然后更新缓存->返回->结束
    if (cacheResponse != null) {
      if (networkResponse?.code == HTTP_NOT_MODIFIED) {
        val response = cacheResponse.newBuilder()
            .headers(combine(cacheResponse.headers, networkResponse.headers))//结合header
            .sentRequestAtMillis(networkResponse.sentRequestAtMillis)//请求事件
            .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,说明服务端资源有更新,就关闭缓存body
        cacheResponse.body.closeQuietly()
      }
    }

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

    if (cache != null) {
      //网络响应可缓存(请求和响应的头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默认只会对get请求进行缓存,因为get请求的数据一般是比较持久的,而post一般是交互操作,没太大意义进行缓存
   //不是get请求就移除缓存
      if (HttpMethod.invalidatesCache(networkRequest.method)) {
        try {
          cache.remove(networkRequest)
        } catch (_: IOException) {
          // The cache cannot be written.
        }
      }
    }
    return response
  }

大概梳理一下步骤:

  1. 从缓存中获取对应请求的响应缓存

  2. 创建CacheStrategy,创建时会判断是否能够使用缓存,在CacheStrategy中有两个成员:networkRequestcacheResponse。他们有如下组合进行判断:

    networkRequest cacheResponse 说明
    Null Null 网络请求、缓存 都不能用,okhttp直接返回504
    Null Not Null 直接使用缓存
    Not Null Null 向服务器发起请求
    Not Null Not Null 向服务器发起请求,若得到响应码为304(无修改),则更新缓存响应并返回
  3. 交给下一个责任链继续处理

  4. 后续工作,返回304则用缓存的响应;否则使用网络响应并缓存本次响应(只缓存Get请求的响应)

缓存拦截器中判断使用缓存或者请求服务器都是通过CacheStrategy判断。分析CacheStrategy前我们先来了解下Http的缓存机制,以便更好的理解:

首次请求:

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

第二次请求:
1726b5f779755ba1_tplv-t2oaga2asx-zoom-in-crop-mark_4536_0_0_0.jpg

上面两张图是对http缓存机制的一个总结:根据 缓存是否过期过期后是否有修改 来决定 请求是否使用缓存。详细内容可看这篇文章: 彻底弄懂HTTP缓存机制及原理

回过头我们再来看CacheStrategy,它的生成代码看起来很简单,只有一行代码:

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

把请求request、获选缓存cacheCandidate传入工厂类Factory,然后调用compute()方法。

  class Factory(
    private val nowMillis: Long,
    internal val request: Request,
    private val cacheResponse: Response?
  ) {
    init {
      if (cacheResponse != null) {
        /*获取候选缓存的请求时间、响应时间,从header中获取 过期时间、修改时间、资源标记等(如果有)*/
        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构造方法内,获取了候选响应的请求时间、响应时间、过期时长、修改时间、资源标记等。

compute方法内部先调用了computeCandidate()获取到缓存策略实例,先跟进到computeCandidate方法中:

/** Returns a strategy to use assuming the request can use the network. */
    private fun computeCandidate(): CacheStrategy {
      // 没有缓存——进行网络请求
      if (cacheResponse == null) {
        return CacheStrategy(request, null)
      }
      //https请求,但没有握手——进行网络请求
      if (request.isHttps && cacheResponse.handshake == null) {
        return CacheStrategy(request, null)
      }

      // 网络响应不可缓存(请求或响应头Cache-Control是'no-store')——进行网络请求
      if (!isCacheable(cacheResponse, request)) {
        return CacheStrategy(request, null)
      }
   //请求头Cache-Control是no-cache或者请求头有"If-Modified-Since"或"If-None-Match"——进行网络请求
    //意思是不使用缓存或者请求手动添加了头部"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()
     //比较请求头里有效期,取较小值
      if (requestCaching.maxAgeSeconds != -1) {
        freshMillis = minOf(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds.toLong()))
      }
      //可接受缓存的最小剩余时间(min-fresh表示客户端不愿意接收剩余有效期<=min-fresh的缓存)
      var minFreshMillis: Long = 0
      if (requestCaching.minFreshSeconds != -1) {
        minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds.toLong())
      }
      //可接受缓存的最大过期时间(max-stale指令表示了客户端愿意接收一个过期了的缓存,例如:过期了1小时还可以用)
      var maxStaleMillis: Long = 0
      // 第一个判断:是否要求必须去服务器验证资源状态
      // 第二个判断:获取max-stale值,如果不等于-1,说明缓存过期后还能使用指定的时长
      if (!responseCaching.mustRevalidate && requestCaching.maxStaleSeconds != -1) {
        maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds.toLong())
      }
//如果响应头没有要求忽略本地缓存并且整合后的缓存年龄小于整合后的过期时间,那么缓存就可以用
      if (!responseCaching.noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
        val builder = cacheResponse.newBuilder()
          //没有满足“可接受的最小剩余有效时间”,加个110警告
        if (ageMillis + minFreshMillis >= freshMillis) {
          builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"")
        }
        val oneDayMillis = 24 * 60 * 60 * 1000L
        //isFreshnessLifetimeHeuristic表示没有过期时间且缓存的年龄大于一天,就加个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
        }
        //都没有,就执行常规网络请求
        else -> return CacheStrategy(request, null) // No condition! Make a regular request.
      }
   // 如果有,就添加到网络请求的头部
      val conditionalRequestHeaders = request.headers.newBuilder()
      conditionalRequestHeaders.addLenient(conditionName, conditionValue!!)

      val conditionalRequest = request.newBuilder()
          .headers(conditionalRequestHeaders.build())
          .build()
      //conditionalRequest(条件网络请求):有缓存但过期了,去请求网络询问服务器是否能用,能用侧返回304,不能则正常执行网络请求
      return CacheStrategy(conditionalRequest, cacheResponse)
    }

同样经过一系列判断:

  1. 没有缓存、https请求但没有握手、手动设置不缓存、忽略缓存或者手动配置缓存过期,都是直接进行网络请求。
  2. 1中都不满足时,如果缓存没有过期,则使用缓存(可能会添加警告)。
  3. 1中都不满足时,如果缓存过期了,但响应头有Etag,Last-Modified,Date,就添加这些header进行条件网络请求。
  4. 1中都不满足时,如果缓存过期了,且响应头没有设置Etag,Last-Modified,Date,就进行常规网络请求。

回头接着再看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()获取到缓存策略候选后,又进行了一个判断:使用网络请求 但是 原请求配置了只能使用缓存,按照上面的分析,此时即使有缓存,也是过期的缓存,所以又new了实例,两个值都为null。

到这里okhttp的缓存机制源码就看完了。okhttp的缓存机制是符合开头http的缓存机制那两张图的,只是增加很多细节判断。

另外注意,缓存的读写是通过 InternalCache完成的。InternalCache是在创建CacheInterceptor实例时 用client.internalCache()作为参数传入。而InternalCache是okhttp内部使用,类似一个代理,InternalCache的实例是 类的属性。Cache 是我们初始化 OkHttpClient时传入的。所以如果没有传入Cache实例是没有缓存功能的。

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

缓存的增删改查,Cache是通过okhttp内部的DiskLruCache实现的,原理和jakewharton的DiskLruCache是一致的,这里就不再叙述。

结语

本篇文章讲解了三个拦截器工作原理,分别是:RetryAndFollowUpInterceptor、BridgeInterceptor、CacheInterceptor。由于篇幅原因,剩下两个拦截器:ConnectInterceptor、CallServerInterceptor,放在下一篇进行讲解。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容