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...

相关推荐
洛克大航海1 天前
Ajax基本使用
java·javascript·ajax·okhttp
whltaoin7 天前
Java 网络请求 Jar 包选型指南:从基础到实战
java·http·okhttp·网络请求·retrofit
华农第一蒟蒻8 天前
谈谈跨域问题
java·后端·nginx·安全·okhttp·c5全栈
一直向钱10 天前
android 基于okhttp的socket封装
android·okhttp
linuxxx11010 天前
ajax回调钩子的使用简介
okhttp
一直向钱11 天前
android 基于okhttp 封装一个websocket管理模块,方便开发和使用
android·websocket·okhttp
linuxxx11012 天前
ajax() 回调函数参数详解
前端·ajax·okhttp
linuxxx11014 天前
ajax与jQuery是什么关系?
ajax·okhttp·jquery
耀耀_很无聊16 天前
12_OkHttp初体验
okhttp
heeheeai16 天前
okhttp使用指南
okhttp·kotlin·教程