Okhttp缓存解析和使用

Okhttp缓存解析和使用

大鱼 13,158 2020-12-10

一.需求

网络缓存应该是常见的需求,服务端和浏览器里面比较常见,客户端也有。

这不,新app产品让首页缓存,网络状况不好,先展示缓存数据。

不怕,okhttp有对缓存做支持

36c9d602b0ddfd80.jpg

找到okhttp官方文档,看到这段代码

  private val client: OkHttpClient = OkHttpClient.Builder()
      .cache(Cache(
          directory = File(application.cacheDir, "http_cache"),
          // $0.05 worth of phone storage in 2020
          maxSize = 50L * 1024L * 1024L // 50 MiB
      ))
      .build()

不就配置一下HttpClient就行嘛,搞起来
然而断网之后并没有出现想要的首页数据
b08e4007bc117f2f.jpg

稳重稳重,毕竟是一个资(咸)深(鱼)开发工程师,耐心一点看看源码吧
9fe907d1fbbe5526509fe7089cf72e4c.jpg

二.源码

1. CacheInterceptor拦截器

OkHttp一个重要的设计模式是责任链,CacheInterceptor就是其中的一个链扣,okhttp大致的源码结构相信大家都看过。下面是CacheInterceptor的源码

@Throws(IOException::class)
  override fun intercept(chain: Interceptor.Chain): Response {
	//1.通过 cache 找到之前缓存的响应,Candidate字面意思应该是一个候选人
    val cacheCandidate = cache?.get(chain.request())
	//2.当前系统时间
    val now = System.currentTimeMillis()
 	//3.通过 CacheStrategy 的工厂方法构造出 CacheStrategy 对象
    val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()
	//4.在 CacheStrategy 的构造过程中,会初始化 networkRequest 和 cacheResponse 这两个变量,分别表示要发起的网络请求和确定的缓存
    val networkRequest = strategy.networkRequest
    val cacheResponse = strategy.cacheResponse
	
    cache?.trackResponse(strategy)
	//5.如果曾经有候选的缓存,但是经过处理后 cacheResponse 不存在,那么关闭候选的缓存资源
    if (cacheCandidate != null && cacheResponse == null) {
      // The cache candidate wasn't applicable. Close it.
      cacheCandidate.body?.closeQuietly()
    }

    //6.如果要发起的请求为空,并且没有缓存,那么直接返回 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)")
          .body(EMPTY_RESPONSE)
          .sentRequestAtMillis(-1L)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build()
    }

    //7.如果不需要发起网络请求,那么直接将缓存返回给调用者。
    if (networkRequest == null) {
      return cacheResponse!!.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build()
    }

    var networkResponse: Response? = null
    try {
	//8.继续调用链的下一个步骤,按常理来说,走到这里就会真正地发起网络请求了。
      networkResponse = chain.proceed(networkRequest)
    } finally {
	//9.保证在发生了异常的情况下,候选的缓存可以正常关闭。
      if (networkResponse == null && cacheCandidate != null) {
        cacheCandidate.body?.closeQuietly()
      }
    }

    //10.网络请求完成之后,假如之前有缓存,那么首先进行一些额外的处理。
    if (cacheResponse != null) {
	//11. 假如是 304,那么根据缓存构造出返回的结果给调用者。
      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 {
	//12. 关闭缓存
        cacheResponse.body?.closeQuietly()
      }
    }
	//13.构造出返回结果。
    val response = networkResponse!!.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build()

    if (cache != null) {
	//14.如果符合缓存的要求,那么就缓存该结果。
      if (response.promisesBody() && CacheStrategy.isCacheable(response, networkRequest)) {
        // Offer this request to the cache.
        val cacheRequest = cache.put(response)
        return cacheWritingResponse(cacheRequest, response)
      }
	//15.对于某些请求方法,需要移除缓存,例如 PUT/PATCH/POST/DELETE/MOVE
      if (HttpMethod.invalidatesCache(networkRequest.method)) {
        try {
          cache.remove(networkRequest)
        } catch (_: IOException) {
          // The cache cannot be written.
        }
      }
    }

    return response
  }

相信看了代码以及代码中的注释,已经大致说明了CacheInterceptor的工作流程,还不明白的话,看下图
D620C0689D9343CA88BEBE782A0E63AE.jpg

流程大致清除了, 但是后面的判断大部分都是根据networkRequest和cacheResponse进行的,这哥俩怎么来的呢?下面看CacheStrategy

2.CacheStrategy 缓存策略工厂

根据上面的源码,CacheStrategy创建的入口在这里

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

直接先看compute方法

fun compute(): CacheStrategy {
      val candidate = computeCandidate()
	//如果网络请求不为空,但是 request 设置了 onlyIfCached 标志位,那么把两个请求都赋值为空。
      if (candidate.networkRequest != null && request.cacheControl.onlyIfCached) {
        return CacheStrategy(null, null)
      }
      return candidate
}

可以看到重点是computeCandidate方法

 private fun computeCandidate(): CacheStrategy {
      //1.如果缓存为空,那么直接返回带有网络请求的策略
      if (cacheResponse == null) {
        return CacheStrategy(request, null)
      }

      //2.Https 请求,但是 cacheResponse 的 handshake 为空。
      if (request.isHttps && cacheResponse.handshake == null) {
        return CacheStrategy(request, null)
      }

       //3.根据缓存的状态判断是否需要该缓存,在规则一致的时候一般不会在这一步返回。
      if (!isCacheable(cacheResponse, request)) {
        return CacheStrategy(request, null)
      }
	//4.获得当前请求的 cacheControl,如果配置了不缓存,或者当前的请求配置了 If-Modified-Since/If-None-Match 字段
      val requestCaching = request.cacheControl
      if (requestCaching.noCache || hasConditions(request)) {
        return CacheStrategy(request, null)
      }

      val responseCaching = cacheResponse.cacheControl
	//5. 计算缓存的年龄。
      val ageMillis = cacheResponseAge()
	//6. 计算刷新时机
      var freshMillis = computeFreshnessLifetime()
	//7.请求所允许的最大年龄。
      if (requestCaching.maxAgeSeconds != -1) {
        freshMillis = minOf(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds.toLong()))
      }
 	//8.请求所允许的最小年龄。
      var minFreshMillis: Long = 0
      if (requestCaching.minFreshSeconds != -1) {
        minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds.toLong())
      }
	//9.最大的 Stale() 时间。
      var maxStaleMillis: Long = 0
      if (!responseCaching.mustRevalidate && requestCaching.maxStaleSeconds != -1) {
        maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds.toLong())
      }
	//10.根据几个时间点确定是否返回缓存,并且去掉网络请求,如果客户端需要强行去掉网络请求,那么就是修改这个条件。
      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())
      }

       //填入条件请求的字段。
      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)
    }

CacheStrategy的执行步骤大致就是这样了,看着源码和注释应该看得懂吧,看不懂就再看两遍,(偷懒不画图了_

a35a12dd5bb95c5d.jpg

流程是看懂了,但是具体是怎么缓存的呢? 从上面的源码我们看到缓存和检测的入口都调用了isCacheable()方法,而进到这个方法里面我们可以看到,用的最多的变量就是response.cacheControl。
接下来看一下一个重要的类,CacheControl

3.CacheControl 缓存控制

英文解释为 A Cache-Control header with cache directives from a server or client
了解http请求的都知道,http请求头Cache-Control,用来控制缓存的方式和策略,一般由服务端控制,一般浏览器都进行了兼容。
Cache-Control字段都有啥,代表啥意思呢? 来来来,打开百度搜索Cache-Control,然后给大家摘来两张图

在请求中使用Cache-Control 时,它可选的值有:
20180921155944100.jpg
在响应中使用Cache-Control 时,它可选的值有:
2018092116013247.jpg

这个时候我们发现,我们对缓存的做法都是根据这个Cache-Control啊,那这个缓存策略岂不是需要服务端来处理吗?
理论上是这样的,但并不是所有的服务端开发人员都会配合客户端的开发,毕竟大家都不愿意承担额外的工作量和风(绩)险(效),你懂得...

所以很多情况下,这个工作就落到了客户端的身上。

三.缓存实现

看了这么多的代码,我们已经知道,只要控制Cache-Control就可以,所以我们只要在责任链中加入我们的interceptor来处理好Cache-Control就可以了

    val netCacheInterceptor: Interceptor = object : Interceptor {
        @Throws(IOException::class)
        override fun intercept(chain: Interceptor.Chain): Response{

            val request: Request = chain.request()
            val response: Response = chain.proceed(request)
            val onlineCacheTime = 24 * 60 * 60 //在线的时候的缓存过期时间,如果想要不缓存,直接时间设置为0
            return response.newBuilder()
                .removeHeader("Pragma")
                .header("Cache-Control", "public, max-age=$onlineCacheTime")
                .build()
        }
    }

	//然后在OkHttpClient添加Interceptor
	OkHttpClient.Builder.addNetworkInterceptor(netCacheInterceptor)

但是我们会发现,直接断网的时候并没有显示网络缓存。
通过CacheInterceptor的源码,我们可以发现,当发生504或者缓存没有命中,但是网络请求失败的时候,其实是得不到任何的返回结果的,如果我们需要在这种情况下返回缓存,那么还需要额外的处理逻辑。
处理方式是,再添加一个Interceptor

 /**
     * 没有网时候的缓存
     */
    val offlineCacheInterceptor: Interceptor = object : Interceptor {
        @Throws(IOException::class)
        override fun intercept(chain: Interceptor.Chain): Response {
            var request: Request = chain.request()
            if (!NetworkUtils.isNetConnected(BaseApplication.instance)) {
                val offlineCacheTime = 2 * 24 * 60 * 60 //离线的时候的缓存的过期时间
                request =
                    request.newBuilder()
                        .removeHeader("Pragma")
                        .header(
                            "Cache-Control",
                            "public, only-if-cached, max-stale=$offlineCacheTime"
                        )
                        .build()
            }
            return chain.proceed(request)
        }
    }
//添加Interceptor
   OkHttpClient.Builder.addInterceptor(offlineCacheInterceptor)

这样就可以实现接口的缓存了。

注意

  1. 缓存只支持GET请求,这段代码在Cache类的put方法里面,如果不是GET请求直接return了,并没有写入缓存
  2. GET请求中不能放置随机参数,比如一些签名和加密信息,因为缓存是以url为key的

Over