OkHttp3笔记---CacheInterceptor

CacheInterceptor概览

  CacheInterceptor的作用为将Http的请求结果放到到缓存中,以便在下次进行相同的Http请求时,直接从缓存中读取结果,避免进行耗时的Http请求。OkHttp3使用缓存的方式如下:

val client:OkHttpClient by lazy {
    private var file = context.externalCacheDir
    OkHttpClient.Builder()
         //缓存大小为10M
        .cache(Cache(File(file,"okHttpCache"),10*1024*1024))
        .build()
}

  在构建Request时,可以使用CacheControl指定缓存的方式。参考OkHttp3缓存

        var request = Request.Builder()
            .url(url)
            //强制使用缓存
            .cacheControl(CacheControl.FORCE_CACHE)
            .get()
            .build()

  在指定缓存文件和缓存大小后,OkHttp只是可能会对Http请求结果进行缓存(取决于Http的类型)。同时,还要留意,在下次进行相同请求时,OkHttp也只是可能会从缓存中读取数据(取决于缓存策略)。以下对OkHttp的缓存进行简单介绍:

  一、OkHttp缓存Http请求结果的条件:

    1、Http请求类型必须为GET。对于POST等请求方式,OkHttp是不会对请求结果进行缓存的

    2、Http请求的状态码为200,204等,具体在下个部分介绍。

    3、响应体不能为空。(好像有要求,又好像没要求)

    4、CacheControl中未禁止缓存,默认是可以进行缓存的

  二、OkHttp读取缓存的条件:

    总的来说,OkHttp是否读取缓存与响应头中的Cache-control字段有关。若此字段的值为private、no-cache、must-revalidate,则OkHttp不会读取缓存中的数据,即便OkHttp已经缓存了之前请求到的数据。若此字段的值为max-age,则在max-age指定的时间内,OkHttp会从缓存中读取数据,不进行网络访问。如max-age=5,表示5秒内,OkHttp会从缓存中读取数据。max-age暗含数据的有效时长。除了Cache-control,OkHttp是否读取缓存,还与请求体中的If-Modified-Since标志有关。If-Modified-Since表示在经过服务端验证后,若数据未变化(返回的状态码为304),则OkHttp使用缓存中的数据;若有变化,则使用服务的新发过来的数据。

CacheInterceptor代码分析

  在进行Http请求时,OkHttp会依次调用每个拦截器的intercept方法,cacheInterceptor(缓存拦截器)的intercept方法为

override fun intercept(chain: Interceptor.Chain): Response {
    val call = chain.call()
    //尝试获取之前缓存的数据(可能为空)
    val cacheCandidate = cache?.get(chain.request())

    val now = System.currentTimeMillis()
    //根据当前请求和缓存的数据生成缓存策略(决定数据的来源,从缓存中读取还是从网络加载)
    val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()
    val networkRequest = strategy.networkRequest //不为null,表示从网络中加载
    val cacheResponse = strategy.cacheResponse  //不为null,表示从缓存中读取

    //从缓存或网络中获取数据

    if (cache != null) {
      //缓存加载到的数据
      ......
    }

    return response
  }

  缓存拦截器会尝试从缓存中读取数据,cache表示OkHttp的缓存,其底层由DiskLruCache实现。读取数据的key由Http的URL转化而来,转化方式如下:

fun key(url: HttpUrl): String = url.toString().encodeUtf8().md5().hex()

  接下来,缓存拦截器会根据当前的请求和读取的缓存结果生成一个缓存策略,以决定从缓存中读取数据还是重新从网络中加载数据。策略如下:

  规则1、networkReuqest和cacheResponse皆为null,直接返回一个空的数据

  规则2、networkRequest为null,cahceReponse不为null。使用缓存中的数据

  规则3、netRequest不为null(cahceReponse为null或不为null)。优先从网络中读取数据,进行Http请求。根据返回的状态码的不同,分为2种情况。1)、状态码为304(未修改),使用缓存中的数据。2)状态码不为304,表示数据发送变化,使用新发送过来的数据,并对数据进行缓存。

  以上规则对应的代码如下:

override fun intercept(chain: Interceptor.Chain): Response {
    //......

    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

   
    //规则1
    // 禁止从网络加载数据且cache中的数据有误
    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().also {
            listener.satisfactionFailure(call, it)
          }
    }

    //规则2
    // 不需要从网络加载数据,直接使用缓存数据,场景:加载图片
    if (networkRequest == null) {
      return cacheResponse!!.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build().also {
            listener.cacheHit(call, it)
          }
    }

    //......
    //规则3
    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()
      }
    }

    //缓存中有数据,检查数据是否发生变化
    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()

        //响应头可能发生变化,更新缓存中的数据
        cache!!.trackConditionalCacheHit()
        cache.update(cacheResponse, response)
        return response.also {
          listener.cacheHit(call, it)
        }
      } else {
        cacheResponse.body?.closeQuietly()
      }
    }
    //数据发生变化,场景:搜索操作->url相同的搜索,通常保存上次的请求结果,对于当前搜索请求,仍然从网络加载数据
    val response = networkResponse!!.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build()

    if (cache != null) {
      //缓存数据
      ......
    }

    return response
  }

  缓存拦截器通过compute方法来生成缓存策略,方法代码如下:

    fun compute(): CacheStrategy {
      val candidate = computeCandidate()

      //禁止从网络加载且缓存有误
      if (candidate.networkRequest != null && request.cacheControl.onlyIfCached) {
        return CacheStrategy(null, null)
      }

      return candidate
    }

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

      //检查cacheResponse是否应被缓存,若否,则使用网络加载数据
      if (!isCacheable(cacheResponse, request)) {
        return CacheStrategy(request, null)
      }
      //cacheControl设置为不缓存,则使用网络加载数据
      val requestCaching = request.cacheControl
      if (requestCaching.noCache || hasConditions(request)) {
        return CacheStrategy(request, null)
      }
      //读取响应头中的Cache-control字段,若为:nocache,private,则从网络加载数据
      val responseCaching = cacheResponse.cacheControl

      val ageMillis = cacheResponseAge()
      //读取Cache-control中的max-age的值,表示数据的有效时间
      var freshMillis = computeFreshnessLifetime()

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

      ......
      
      if (!responseCaching.noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
        //使用缓存中的数据
        ......
        return CacheStrategy(null, builder.build())
      }

      // 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!!)

      val conditionalRequest = request.newBuilder()
          .headers(conditionalRequestHeaders.build())
          .build()
      //网络与缓存都有数据,优先使用网络加载数据
      return CacheStrategy(conditionalRequest, cacheResponse)
    }

  上面方法中调用了isCacheable方法,其代码如下,作用为 决定请求结果是否要进行缓存,返回true表示缓存请求结果,返回false则表示不缓存

  

fun isCacheable(response: Response, request: Request): Boolean {
      // 缓存要满足的2个条件:1、状态码满足when的第一部分;2、response与request的CacheControl皆设置为允许缓存
      when (response.code) {
        HTTP_OK,
        HTTP_NOT_AUTHORITATIVE,
        HTTP_NO_CONTENT,
        HTTP_MULT_CHOICE,
        HTTP_MOVED_PERM,
        HTTP_NOT_FOUND,
        HTTP_BAD_METHOD,
        HTTP_GONE,
        HTTP_REQ_TOO_LONG,
        HTTP_NOT_IMPLEMENTED,
        StatusLine.HTTP_PERM_REDIRECT -> {
          // These codes can be cached unless headers forbid it.
        }

        HTTP_MOVED_TEMP,
        StatusLine.HTTP_TEMP_REDIRECT -> {
          // These codes can only be cached with the right response headers.
          // http://tools.ietf.org/html/rfc7234#section-3
          // s-maxage is not checked because OkHttp is a private cache that should ignore s-maxage.
          if (response.header("Expires") == null &&
              response.cacheControl.maxAgeSeconds == -1 &&
              !response.cacheControl.isPublic &&
              !response.cacheControl.isPrivate) {
            return false
          }
        }

        else -> {
          // All other codes cannot be cached.
          return false
        }
      }

      // A 'no-store' directive on request or response prevents the response from being cached.
      return !response.cacheControl.noStore && !request.cacheControl.noStore
    }

  至此,对缓存拦截器的“缓存策略”分析就完成了。

  在前面提到过,在网络加载到数据后,OkHttp会对数据进行缓存,接下来,看下OkHttp如何缓存加载到的数据。

override fun intercept(chain: Interceptor.Chain): Response {
    val call = chain.call()
    val cacheCandidate = cache?.get(chain.request())

    ......

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

    ......
    //生成要缓存的数据response
    val response = networkResponse!!.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build()

    if (cache != null) {
      //检查是否要缓存response。要求1、response有响应体;2、isCachheable返回true
      if (response.promisesBody() && CacheStrategy.isCacheable(response, networkRequest)) {
        // 再次检查response是否符合缓存条件,并进行缓存
        val cacheRequest = cache.put(response)
        //封装response
        return cacheWritingResponse(cacheRequest, response).also {
          if (cacheResponse != null) {
            // This will log a conditional cache miss only.
            listener.cacheMiss(call)
          }
        }
      }
      
      if (HttpMethod.invalidatesCache(networkRequest.method)) {
        //删除cache中不符合要求的response
        try {
          cache.remove(networkRequest)
        } catch (_: IOException) {
          // The cache cannot be written.
        }
      }
    }

    return response
  }

  上面的方法调用了cache的put方法,此put方法会会检查response是否符合缓存条件,并对其进行缓存。put方法代码如下:

internal fun put(response: Response): CacheRequest? {
    val requestMethod = response.request.method

    //检查http请求的类型,POST,PATCH,DELTE,MOVE,PUT类型不缓存
    if (HttpMethod.invalidatesCache(response.request.method)) {
      try {
        remove(response.request)
      } catch (_: IOException) {
        // The cache cannot be written.
      }
      return null
    }

    //非GET类型,不缓存
    if (requestMethod != "GET") {
      // Don't cache non-GET responses. We're technically allowed to cache HEAD requests and some
      // POST requests, but the complexity of doing so is high and the benefit is low.
      return null
    }

    if (response.hasVaryAll()) {
      return null
    }
    //缓存response
    val entry = Entry(response)
    var editor: DiskLruCache.Editor? = null
    try {
      editor = cache.edit(key(response.request.url)) ?: return null
      entry.writeTo(editor)
      return RealCacheRequest(editor)
    } catch (_: IOException) {
      abortQuietly(editor)
      return null
    }
  }

  至此,缓存拦截器的拦截策略,缓存过程,缓存的读取条件及过程都已分析完成。
 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值