OkHttp缓存策略解析

HTTP缓存

在Http协议中,缓存的控制是通过headerCache-Control来控制,通过对Cache-Control进行设置,即可实现不同的缓存策略。

Cache-Control是一个通用的header字段,可以在请求头中使用,也可以在响应头中使用。其中请求指令集和响应指令集有重合的部分,也有不同的部分。

常用的请求指令集:

  • no-cache: 不使用缓存;

  • max-age: 缓存时间;

  • max-stale:缓存过期后多长时间数据仍然有效;

  • min-refresh: 最短刷新时间;

  • only-if-cache: 表示直接获取缓存数据,若没有数据返回,则返回504

常用的响应头指令集:

  • no-cache: 不使用缓存;

  • max-age: 缓存时间;

  • must-revalidate: 访问缓存数据时,需要先向源服务器确认缓存数据是否有效,如无法验证其有效性,则需返回504。需要注意的是:如果使用此值,则max-stale将无效。

上述指令集我们先扫一眼即可,下文还会详细说明。

OkHttp拦截器

我们知道OkHttp利用不同的拦截器,通过责任链模式顺序执行,逆序返回处理结果。而CacheInterceptor处于中间的位置:

我们可以看一下CacheInterceptor中的interceptor方法:

scss 复制代码
override fun intercept(chain: Interceptor.Chain): Response {
  val call = chain.call()
  // 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

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

  // 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)")
        .sentRequestAtMillis(-1L)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build().also {
listener.satisfactionFailure(call, it)
        }
}

  // If we don't need the network, we're done.
  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)
  }

  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 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(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 {
      cacheResponse.body.closeQuietly()
    }
  }

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

  if (cache != null) {
    if (response.promisesBody() && CacheStrategy.isCacheable(response, networkRequest)) {        
    // 4. 
      // Offer this request to the cache.
      val cacheRequest = cache.put(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)) {
      try {
        cache.remove(networkRequest)
      } catch (_: IOException) {
        // The cache cannot be written.
      }
    }
  }

  return response
}
  1. 从缓存中取出请求对应的缓存数据(Response对象,第一次请求,该Response为空),此时还未判断其是否有效,因此时候选状态;

  2. 构造一个CacheStrategy.Factory对象,参数包括当前时间,请求,以及候选Response,而后调用Factory.compute方法,返回CacheStrategy对象:

    vbscript 复制代码
      class CacheStrategy internal constructor(
        /** The request to send on the network, or null if this call doesn't use the network. */
      val networkRequest: Request?,
        /** The cached response to return or validate; or null if this call doesn't use a cache. */
      val cacheResponse: Response?
      )
      ```

CacheStrategy对象有两个参数:

  • networkRequestRequest对象,为null时表示没有使用网络
  • cacheResponseResponse对象,为null时表示没有使用缓存
  1. 根据CacheStrategy对象中的networkRequestcacheResponse值排列组合,得到不同情况下的处理结果:
  1. 判断网络请求结果是否需要放入缓存,需要的话加入缓存中

缓存入口

CacheStrategy.Factory.compute

kotlin 复制代码
 /** Returns a strategy to satisfy [request] using [cacheResponse]. */
fun compute(): CacheStrategy {
   // 1. 
  val candidate = computeCandidate()

    // 2. 
  // 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. 核心调用了computeCandidate()方法获取候选者;

  2. 这里碰到了第一个指令集"onlyIfCached",这里的意思是,当我们使用网络且设置请求指令onlyIfCached为true时,我们将返回networkRequestcacheResponse值均为null的CacheStrategy,这将使CacheInterceptor.interceptor中返回code码为504:

    scss 复制代码
       // 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) // 504
             .message("Unsatisfiable Request (only-if-cached)")
             .sentRequestAtMillis(-1L)
             .receivedResponseAtMillis(System.currentTimeMillis())
             .build().also {
       listener.satisfactionFailure(call, it)
             }
       }
       ```

缓存策略解析及过期判断

  • computeCandidate()
kotlin 复制代码
private fun computeCandidate(): CacheStrategy {

    //1. 
  // No cached response.
  if (cacheResponse == null) {
    return CacheStrategy(request, null)
  }

  // Drop the cached response if it's missing a required handshake.
  if (request.isHttps && cacheResponse.handshake == null) {
    return CacheStrategy(request, null)
  }

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

  val requestCaching = request.cacheControl
  if (requestCaching.noCache || hasConditions(request)) {
    return CacheStrategy(request, null)
  }

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

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

  // 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)
}
  1. 根据cacheResponserequest值是否为空,以及请求指令中的"no-cache"/"If-Modified-Since"/"If-None-Match"的具体值(我们暂时不需要关心后两个指令),来返回CacheStrategy(request, null)对象;

  2. 主要是根据请求指令集和响应指令集来判断缓存是否过期,我们可以摘出部分代码分析:

    kotlin 复制代码
       // a. 
       val ageMillis = cacheResponseAge()
    
       // b.
         var freshMillis = computeFreshnessLifetime()
    
         if (requestCaching.maxAgeSeconds != -1) {
           freshMillis = minOf(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds.toLong()))
         }
    
       // c. 
         var minFreshMillis: Long = 0
         if (requestCaching.minFreshSeconds != -1) {
           minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds.toLong())
         }
    
       //d. 
         var maxStaleMillis: Long = 0
         if (!responseCaching.mustRevalidate && requestCaching.maxStaleSeconds != -1) {
           maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds.toLong())
         }
    
       // e. 
         if (!responseCaching.noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
           val builder = cacheResponse.newBuilder()
           //f. 
           if (ageMillis + minFreshMillis >= freshMillis) {
             builder.addHeader("Warning", "110 HttpURLConnection "Response is stale"")
           }
           val oneDayMillis = 24 * 60 * 60 * 1000L
           //g.
           if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
             builder.addHeader("Warning", "113 HttpURLConnection "Heuristic expiration"")
           }
           return CacheStrategy(null, builder.build())
         }
       ```
  • 计算缓存的存在时间ageMillis
  • 计算最大过期时间freshMillis,该值是通用指令"max-age"在请求指令集以及响应指令集中的最小值,当在请求指令集中不存在时,取响应指令集中的值,如果响应指令集中也没有设置,将有另一套计算方法,这里暂且不表;
  • 计算最短刷新时间minFreshMillis,取的是请求头中"min-refresh"的值;
  • 计算缓存过期后最大有效市场maxStaleMillis,取的是请求头中"max-stale"的值;
  • ageMillis + minFreshMillis < freshMillis + maxStaleMillis,缓存有效;
  • ageMillis + minFreshMillis >= freshMillis,会在响应头中加入"Warning"字段,表示缓存已经过期了,马上就要失效了;

至此,我们就可以判断什么时候会返回缓存数据,什么时候会去网络请求最新数据有了一个较为清晰的认知。

OkHttp缓存如何使用

根据上文,我们已经知道OKHttp对于缓存是如何处理的,主要是根据请求头和响应头中的"Cache-Control"指令,利用CacheInterceptor拦截器完成缓存数据的处理,因此,我们基于责任链模式,添加自定义拦截器,实现缓存控制,一方面我们需要对请求头进行设置,这部分拦截器需要设置CacheInterceptor的左侧(链条的顺序),通过addInterceptor完成添加,在另一方面,我们需要对响应头进行设置,这个拦截器设置在CacheInterceptor的右侧,通过addNetworkdInterceptor完成。

java 复制代码
// 响应头设置
public class HttpCacheInterceptor implements Interceptor {

        private Context context;

        public HttpCacheInterceptor(Context context) {
                this.context = context;
        }

        @Override
        public Response intercept(Chain chain) throws IOException {
                return chain.proceed(chain.request()).newBuilder()
                        .request(newRequest)
                        .removeHeader("Pragma")
                        .header("Cache-Control", "public, max-age=" + 1)
                        .build();
        }
}


// 请求头设置
public class BaseInterceptor implements Interceptor {

        private Context mContext;

        public BaseInterceptor(Context context) {
                this.mContext = context;
        }

        @Override
        public Response intercept(Chain chain) throws IOException {
        
                if (NetworkUtil.isConnected(mContext)) {
                    return chain.proceed(chain.request());        
                } else { // 如果没有网络,则返回缓存未过期一个月的数据
                    Request newRequest = chain.request().newBuilder()
                            .removeHeader("Pragma")
                            .header("Cache-Control", "only-if-cached, max-stale=" + 30 * 24 * 60 * 60);
                    return chain.proceed(newRequest);        
                }
        }
}


OkHttpClient httpClient = new OkHttpClient.Builder()
        .addInterceptor(new BaseInterceptor(context))
        .addNetworkInterceptor(new HttpCacheInterceptor(context))
        .cache(new Cache(context.getCacheDir(), 20 * 1024 * 1024)) // 设置缓存路径和缓存容量
        .build();

参考文档

juejin.cn/post/684490...

相关推荐
陶甜也12 小时前
前后端分离,使用MOCK进行数据模拟开发,让前端攻城师独立于后端进行开发
前端·okhttp
Python私教1 天前
JavaScript 基于生成器的异步编程方案相关代码分享
android·javascript·okhttp
+码农快讯+2 天前
JavaScript 基础 - 第16天_AJAX入门
javascript·ajax·okhttp
失落夏天3 天前
OKHttp实现原理分享
okhttp
铁打的阿秀5 天前
okhttp 报java.lang.IllegalStateException: closed
java·开发语言·okhttp
Mac Zhu6 天前
okHttp下载文件到本地存储
okhttp
文韬_武略7 天前
OkHttp Interceptor日志上报
okhttp
~ 小团子9 天前
JavaWeb系列二十一: 数据交换和异步请求(JSON, Ajax)
ajax·okhttp·json
kejizhentan12 天前
前端技术(六)—— AJAX详解
前端·ajax·okhttp
追梦的鱼儿14 天前
okhttp 拦截器用过那些? 什么情况下用的?如何使用?
okhttp·拦截器