二、深入理解OKHttp:缓存处理-CacheIntercepter

6,333 阅读12分钟

一、前言

【1.1】OkHttp系列其他篇章:

  1. 同步请求的实现流程
  2. 异步请求的实现流程
  3. 重要拦截器:CacheInterceptor 的解析
  4. 重要拦截器:ConnectInterceptor 的解析
  5. 重要拦截器:CallServerInterceptor 的解析

【1.2】陈述

OkHttp中提供了网络请求的缓存机制,当我们在上篇中追溯请求的流程时,知道每个Request都需要进过CacheInterceptor.process()的处理,但是整个缓存处理肯定是不止缓存拦截器的这一个方法的逻辑,它还涉及到:

  • Http缓存机制,对应CacheStrategy 类
  • LRUCache/DiskLRUCahche:对缓存进行高效增删改查。
  • okio:进行IO处理。
    在开始 CacheInterceptor 的源码解析前,我们需要先了解 Http 缓存机制才能明白CacheStrategy类的存在意义。 而本篇只介绍Http的缓存机制和OkHttp中的处理。对于LruCache和okio以后会单拎开篇。

二、Http 缓存机制

【2.1】 为什么需要缓存

让我们设想一个场景:一个用户一天内都打开多次某个页面,而这个页面的内容相对固定,并不是每次都更改。那么我们有必要每次都从服务器中下载资源吗?答案是不用的。此时缓存就排上用场了。上面场景只是缓存的其中的一个好处,合理的使用缓存还能有如下好处:

  1. 优化用户体验,避免空白页面的展示,提供默认数据展示。
  2. 避免不必要的访问服务器,减轻宽带负担。

【2.2】缓存分类之强制缓存

【2.2.1】简介: 一般地,当客户端向服务端请求时,按照是否重新向服务器发起请求来划分,那么有强制缓存和协商缓存两种缓存类型。他们的优劣势各不相同。而强制缓存:当缓存处在且未失效的条件下,直接使用缓存作为返回而且http返回的状态码为200,否则请求服务器。简要的请求流程如下:

【2.2.2】优缺点:

  • 优点:加载速度快,性能好。
  • 缺点:在缓存没失效前,都不会请求服务器。如果服务器此时更新了资源,客户端得不到最新的响应。

【2.2.3】相关请求头

  1. Pragma: no-cache。代表禁用缓存,目前是在HTTP1.1中已被废弃。
  2. Expires: GMT时间。代表改缓存的有效时间。可兼容HTTP/1.0和HTTP1.1。但是由于这个时间是服务器给的,会出现服务器和客户端时间不一致的问题。

【2.3】缓存分类之协商缓存

【2.3.1】简介: 协商缓存:当缓存存在时,带上用缓存标识先向服务器请求,服务器对比资源标识,如果不需要下发新资源,那么会直接返回304状态码,告诉客户端可用缓存;否则将新的资源和新的资源标识一起返回,此时的状态码为200。简要的请求流程如下:

【2.3.2】优缺点:

  • 优点:减少服务器数据传输压力。能够及时更新数据。
  • 缺点:每次都需要想服务器请求一次判断资源是否最新。
【2.3.3】相关请求头
  1. Last-Modified/If-Modified-Since:xxx 在服务器首次请求回来的数据的请求头,附带了Last-Modified:xxx。这个时间值会在下次请求时,被附带在If-Modified-Since的请求值里。服务器对比两个值,如果一至就返回304状态码,告知客户端继续使用缓存。如果不一致,服务器返回新的Expires和Last-Modifed。缺点:Last-Modified只能精确到秒级,如果一个文件在1s内被更改,那么他们的值Last-Modified值是一样的,这会导致更新不到新资源问题。
  2. ETag/If-Not-Match: 鉴于上面Last-Modified的缺点,增加了一个新的字段。服务器通过某种算法,对资源进行计算,比如MD5,然后赋值在Etag返回到客户端。客户端下次请求将值赋值到If-Not-Match/或者If-Match上,服务器进行比较,如果一致则直接返回304状态码,通知客户端可以使用缓存。如果需要更新,那么状态码为200,并返回整个新的资源。并且他们的优先级高于Last-Modified/If-Modified-Since

有了上面的一些Http缓存基本知识,接下来就可以跟随Okhttp的代码,来看看它是怎么处理缓存的了。

【2.4】缓存控制:CacheControl

【2.4.1】当它在请求头时
可选字段 意义
no-cache 不使用缓存,直接向服务器发起请求。
no-store 不储存缓存
max-age = xxx 告诉服务器,请求一个存在时间不超过xxx秒的资源
max-stale = xxx 告诉服务器,可接受一个超过缓存时间为xxx秒的资源,如果xxx秒没有定义,则时间为任意时间
min-fresh = xxx 告诉服务器,希望接收一个在小于xxx秒内被更新过的资源
【2.4.1】当它在响应头时
header 1 header 2
可选字段 意义
no-cache 不直接使用缓存,需要向服务器发起请求校验缓存。
no-store 服务器告诉客户端不缓存响应
no-transform 告知客户端在缓存响应时,不得对数据做改变
only-if-cached 告知客户端不进行网络请求,只使用缓存,如果缓存不命中,那么返回503状态码
Max-age=xxx 告知客户端,该响应在xxx秒内是合法的,不需要向服务器发起请求。
public 表示任何情况下都缓存该响应
private=“xxx” 表示xxx或者不指明是为全部,值对部分用户做缓存。

三、OkHttp的缓存处理

【3.1】CacheInterceptor:缓存开始的地方

CacheInterceptor.java
@Override public Response intercept(Chain chain) throws IOException {
    //1.从LruCache中,根据Request,取出缓存的Response
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();

    //2.缓存选择策略类,根据request和response来决定需不需要使用缓存。
    //new CacheStrategy.Factory() 详见:【3.2】
    //CacheStrategy.Factory.get() 详见:【3.3】
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    //缓存策略逻辑执行后的产物,主要根据这两个对象判断是否使用缓存等。
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;

    //3.缓存跟踪记录缓存策略选择后的结果。
    if (cache != null) {
      cache.trackResponse(strategy);
    }

    //4.缓存数据库里的响应缓存不为空,但是结果缓存策略选择后的结果为空
    //证明这个响应缓存已经过时不适用了,将起关闭,防止内存泄露。后续的操作中也会将不用的Response进行关闭,就不一一赘述。
    if (cacheCandidate != null && cacheResponse == null) {
      closeQuietly(cacheCandidate.body()); 
    }

    // 5.如果禁用了网络,此时request为空,而缓存的响应也为空,直接504的响应
    if (networkRequest == null && cacheResponse == null) {
      return new Response.Builder()
          .request(chain.request())
          .protocol(Protocol.HTTP_1_1)
          .code(504)
          .message("Unsatisfiable Request (only-if-cached)")
          .body(Util.EMPTY_RESPONSE)
          .sentRequestAtMillis(-1L)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build();
    }

    // 6.如果不需要网络,且缓存的响应有效,返回这个缓存的响应。
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }

    //7.到这一步,说明需要执行真正的网络请求,得到网络的响应了,所以执行下一个拦截器逻辑。
    Response networkResponse = null;
    try {
      networkResponse = chain.proceed(networkRequest);
    } finally {
      ...
    }

    // 8.如果缓存的Response不为空,此时要综合网络返回回来的Respnse进行选择。
    if (cacheResponse != null) {
      //当服务器告诉客户端数据没有改变时,客户端直接使用缓存的Response。
      //但是会更新最新的一些请求头等数据到缓存的Response。
      if (networkResponse.code() == HTTP_NOT_MODIFIED) {
        Response response = cacheResponse.newBuilder()
            .headers(combine(cacheResponse.headers(), networkResponse.headers()))
            .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
            .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
            .cacheResponse(stripBody(cacheResponse))
            .networkResponse(stripBody(networkResponse))
            .build();
            ...
        return response;
      } else {
        closeQuietly(cacheResponse.body());
      }
    }

    //9.到这一步,确定使用网络的Response。
    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

    //10.缓存新的Response
    if (cache != null) {
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        // Offer this request to the cache.
        CacheRequest cacheRequest = cache.put(response);
        return cacheWritingResponse(cacheRequest, response);
      }

      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          cache.remove(networkRequest);
        } catch (IOException ignored) {
          // The cache cannot be written.
        }
      }
    }
    //返回最新的Respnse。
    return response;
  }

总结:总的来说,CacheIntercepter根据缓存策略选择出来的Request和Response来决定是否用缓存,和缓存的更新。详细的,它做了如下事情:

  1. 尝试获取缓存的Response。
  2. 将Request和Resonse投放进CacheStrategy,得到要进行网络请求的netRequest和缓存的响应cacheResponse。
  3. 缓存检测上述得到的2个实体。
  4. 如果(禁用网络&&没有缓存),直接返回504的响应。
  5. 如果(禁用网络&&有缓存),使用缓存的响应。
  6. 缓存无效,进行网络请求,得到最新的netReponse。
  7. 如果本地存在缓存,检查netResponse的响应是否为不需要更新。如果是将netResponse的一些响应头等数据更新到cacheResonse并返回缓存的响应。相应的做LRUCache的命中记录。
  8. 如果7没有返回,代表用最新的netResonse作为结果。那么此时更新最新的响应到缓存中,并返回。

【3.2】new CacheStrategy.Factory()

CacheStrategy.Factory.java
 public Factory(long nowMillis, Request request, Response cacheResponse) {
      this.nowMillis = nowMillis;
      this.request = request;
      this.cacheResponse = cacheResponse;

      if (cacheResponse != null) {
        this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
        this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();
        Headers headers = cacheResponse.headers();
        for (int i = 0, size = headers.size(); i < size; i++) {
          String fieldName = headers.name(i);
          String value = headers.value(i);
          if ("Date".equalsIgnoreCase(fieldName)) {
            servedDate = HttpDate.parse(value);
            servedDateString = value;
          } else if ("Expires".equalsIgnoreCase(fieldName)) {
            expires = HttpDate.parse(value);
          } else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
            lastModified = HttpDate.parse(value);
            lastModifiedString = value;
          } else if ("ETag".equalsIgnoreCase(fieldName)) {
            etag = value;
          } else if ("Age".equalsIgnoreCase(fieldName)) {
            ageSeconds = HttpHeaders.parseSeconds(value, -1);
          }
        }
      }

总结: 记录起request和缓存的Resonse,然后解析出cacheReonse响应头里面有关缓存的键值对并保存起来。之前在一中讲到的如ETag、Last-Modified等在这里就出现了。

【3.3】Factory.get()


CacheStrategy.Factory.java
public CacheStrategy get() {
  【详见3.4】获取候选的请求和缓存响应。
  CacheStrategy candidate = getCandidate();

  /**这里如果networkRequest != null 代表缓存不可用,需要进行网络请求。
  但是需要检查cacheControl是否指明了只是用缓存,不用网络。如果是的话,此时综合2个判断,可以得出请求失败。
  而netWorkRequest = null && 擦车 Response = null 的处理结果我们可以在【3.1】的5中可以看到,返回的是504.
  */
  if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
    return new CacheStrategy(null, null);
  }

  return candidate;
}

总结: 这里做获取候选的请求和cacheResponse。并且判断如果需要进行网络请求,并且在只用缓存的情况下,缓存不可用,那么直接返回2个空的候选结果。这里的CacheControl是对于Http里cacheControl请求头字段的描述。

【3.4】获取候选响应:CacheStrategy.Factory.getCandidate()


CacheStrategy.Factory.java
 private CacheStrategy getCandidate() {
  //1. 该request没有缓存的响应,那么返回没有缓存的策略
  if (cacheResponse == null) {
    return new CacheStrategy(request, null);
  }

  //2. 如果请求是Https,并且缓存的握手已经丢失,那么也返回一个没有缓存的策略。
  if (request.isHttps() && cacheResponse.handshake() == null) {
    return new CacheStrategy(request, null);
  }

  //3. 这里进行响应是否可缓存的判断。
  if (!isCacheable(cacheResponse, request)) {
    return new CacheStrategy(request, null);
  }

  //4.如果request要求不用缓存;
  //或者请求里面带有上次请求时服务器返回的"If-Modified-Since" || "If-None-Match"
  //那么他们需要返回一个没有缓存的策略。
  CacheControl requestCaching = request.cacheControl();
  if (requestCaching.noCache() || hasConditions(request)) {
    return new CacheStrategy(request, null);
  }
    

  
  CacheControl responseCaching = cacheResponse.cacheControl();
  //获得响应的年龄
  long ageMillis = cacheResponseAge();
  //获得最近刷新时间
  long freshMillis = computeFreshnessLifetime();

  //最近刷新时间还需要结合请求头里的最大年龄时间,取中间最小值。
  if (requestCaching.maxAgeSeconds() != -1) {
    freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
  }

  //获得请求里的最小刷新时间。
  long minFreshMillis = 0;
  if (requestCaching.minFreshSeconds() != -1) {
    minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
  }

  //获得服务器的最大验证秒数,如果有的话
  long maxStaleMillis = 0;
  if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
    maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
  }


  //5.在缓存响应可被缓存的条件下
  //如果满足(cacheResponse的年龄+最小刷新时间)<(最近刷新时间+最大验证秒数)那么可以不用进行网络请求而直接用cacheResonse。
  if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
    Response.Builder builder = cacheResponse.newBuilder();
    if (ageMillis + minFreshMillis >= freshMillis) {
      builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
    }
    long oneDayMillis = 24 * 60 * 60 * 1000L;
    if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
      builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
    }
    //返回一个不用网络请求,直接用cacheResponse的策略,这时进行强制缓存。
    return new CacheStrategy(null, builder.build());
  }

  /**
  * 6.到这里,进行协商缓存的判断。可以看到它们的优先级是:etag>lastModified>serverDate。
  */
  String conditionName;
  String conditionValue;
  if (etag != null) {
    conditionName = "If-None-Match";
    conditionValue = etag;
  } else if (lastModified != null) {
    conditionName = "If-Modified-Since";
    conditionValue = lastModifiedString;
  } else if (servedDate != null) {
    conditionName = "If-Modified-Since";
    conditionValue = servedDateString;
  } else {
    //如果如果不存在以上请求头,那么它将进行一个常规的网络请求。
    return new CacheStrategy(request, null); 
  }
  
  //如果存在上述说的请求头,那么将他加进入请求里面。
  Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
  Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);

  Request conditionalRequest = request.newBuilder()
      .headers(conditionalRequestHeaders.build())
      .build();
  // 7.返回一个需要进行网络请求并且存在缓存响应的策略,此时他们将进行协商缓存
  return new CacheStrategy(conditionalRequest, cacheResponse);
}

总结: 缓存策略中,针对请求头和缓存响应头的一些值,进行缓存策略的选择,并返回。总的来说他们做了如下判断:

  1. 该请求对应的响应没有缓存:返回无缓存响应策略。
  2. 该请求的cacheRsponse已经丢失握手:返回无缓存响应策略。
  3. cacheResponse是不能缓存的类型,比如响应的响应码不符合规则,或则头部存在"noStore"等情况,这部分可参照isCacheable()方法,这里不赘述。:返回无缓存响应策略。
  4. 满足:(cacheResponse的年龄+最小刷新时间)<(最近刷新时间+最大验证秒数):返回直接用缓存策略。
  5. 该请求没有协商缓存的相关头部:返回常规网络请求策略。
  6. 存在协商缓存相关头部:返回request+cacheResponse策略。

netWorkRequest和cacheResponse的不同组合情况得出的结果如下表:

netWorkRequest cacheResponse 表现结果
不用网络且缓存不可用,直接返回503
不为空 不走网络,直接返回缓存
不为空 缓存不可用,常规请求。
不为空 不为空 需要协商缓存,进行网络访问,进一步确认。

本篇小节: 在本篇中,我们按照是否需要向服务器请求,介绍了Http缓存的2种缓存方式:强制缓存和协商缓存。然后从CacheInterceptor.java入手,输入理解了Okhttp在缓存逻辑中做的一些事情:CacheStrategy获取缓存,CacheInterceptor根据缓存策略获得的候选Request和Response作出响应的逻辑处理,或是返回缓存响应,或是返回错误,或是进行协商缓存等