OkHttp - Interceptors(二)

作者: Joe_H | 来源:发表于2017-04-07 16:08 被阅读0次

本文中源码基于OkHttp 3.6.0

本文主要分析 OkHttp 中的 CacheInterceptor 对缓存的处理。

在实际的网络请求过程中,一份响应数据在一定时间内可能并不会发生修改,如果每次响应都传输同一份数据就会造成冗余的数据传输,浪费服务器的带宽,同时也增加了服务器的性能压力。

那么为了解决这些问题,HTTP 提供了缓存这一机制,在得到原始的响应数据后,本地或者缓存服务器保留原始响应数据的一个副本,如果后续再发起相同的请求,则将响应的副本直接返回给请求方,从而提高响应速度、并减少服务器的压力。

引入缓存后,就需要处理缓存是否命中、缓存是否新鲜等情况,我们可以从下图看看缓存的处理流程。


缓存处理流程

为了让客户端能够判断缓存是否过期,Http 中定义了多个 Header 值来描述缓存的过期时间、以及用来进行服务器缓存再验证。

缓存过期时间:
  • Cache-Control: max-age,描述响应缓存能够存活的最大时间,response 从生成到不再新鲜的时间;
  • Expires,描述缓存过期的绝对时间,如果系统时间超过该时间则表示缓存不再新鲜。

Expires 是 HTTP/1.0+ 定义的 Header,Cache-Control 是 HTTP/1.1 中定义的 Header,它们的本质是一样的,都是用于描述缓存的过期时间,它们最大的区别是 Cache-Control 描述相对时间,Expires 描述绝对时间。

从之前的流程图中知道,在缓存模块匹配到 Request 的缓存后需要判断缓存是否过期,HTTP 中使用缓存的存活时间和新鲜时间来进行判断:

  • 存活时间:表示服务器发布原始响应后经过的总时间;
  • 新鲜时间:缓存在过期之前能过存活的时间。

如果存活时间小于新鲜时间,则表示缓存未过期,反之表示缓存过期,需要进行验证。

条件验证:
  • If-Modified-Since: <Date>,服务器在执行请求时通常会在 Response 中包含一个 Last-Modified 首部表示文档的修改时间,在 If-Modified-Since 后跟上这个时间就能让服务器进行验证文档的有效性了。
  • If-None-Match: <Tag>,有时通过时间进行验证是不够的,Response 中通常会使用 ETag 对实体进行标记,在条件验证时,服务器通过判断标签是否发生变化来确定文档的有效期。

在执行条件验证的时候,在 Request 的 Header 中加上验证条件,服务器在收到请求后,会判断条件是否满足,如果在指定条件下,服务器上的文旦内容发生了改变,服务器将执行一个原始的请求并返回最新的文档数据;如果文档未发生改变,则服务器会返回一个 304 Not Modified 报文,并返回一个新的过期时间用于更新缓存。

- CacheInterceptor

下面我们看看 OkHttp 中 CacheInterceptor 对缓存的处理,先上源码。

public Response intercept(Chain chain) throws IOException {
  Response cacheCandidate = cache != null
      ? cache.get(chain.request())
      : null;

  long now = System.currentTimeMillis();

  CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
  Request networkRequest = strategy.networkRequest;
  Response cacheResponse = strategy.cacheResponse;

  if (cache != null) {
    cache.trackResponse(strategy);
  }

  if (cacheCandidate != null && cacheResponse == null) {
    closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
  }

  // If we're forbidden from using the network and the cache is insufficient, fail.
  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();
  }

  // If we don't need the network, we're done.
  if (networkRequest == null) {
    return cacheResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .build();
  }

  Response networkResponse = 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) {
      closeQuietly(cacheCandidate.body());
    }
  }

  // If we have a cache response too, then we're doing a conditional get.
  if (cacheResponse != null) {
    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();
      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 {
      closeQuietly(cacheResponse.body());
    }
  }

  Response response = networkResponse.newBuilder()
      .cacheResponse(stripBody(cacheResponse))
      .networkResponse(stripBody(networkResponse))
      .build();

  if (HttpHeaders.hasBody(response)) {
    CacheRequest cacheRequest = maybeCache(response, networkResponse.request(), cache);
    response = cacheWritingResponse(cacheRequest, response);
  }

  return response;
}

初看这段代码真的很让人崩溃,特别是那个 CacheStrategy,实在让人摸不着头脑,完全看不出它有任何策略模式的影子。

我们暂时先猜测构建 CacheStrategy 的目的只是为了修改 Request 和 Response 中的属性。

后面的条件判断逻辑也很模糊不清,各种判空作为逻辑条件,如果不去看 CacheStrategy 的源码,不知道缓存的处理流程的话,基本搞不懂这些判断是什么意思。。。

吐槽完了还是得继续,那么我们结合前面分析的缓存处理过程,重新来梳理一遍这段代码。

首先是匹配缓存,这一步很简单。

Response cacheCandidate = cache != null
      ? cache.get(chain.request())
      : null;

这里判断用户是否设置了 Cache,如果存在 Cache,则从 Cache 中匹配当前请求的缓存。
用户可以通过 OkHttpClient 中设置 Cache,其中需要制定缓存的存放路径和缓存的最大容量。

OkHttpClient client = new OkHttpClient.Builder()
      .cache(new Cache(Environment.getDownloadCacheDirectory(), 1024 * 1024)).build();

按道理讲,接下来应该判断 是否命中缓存,从而决定是直接请求网络数据还是判断缓存是否过期,但 CacheInterceptor 中并没有这么做,而是构建了一个 CacheStrategy,实际上它是将缓存的合法性、缓存是否过期等判断全部放到 CacheStrategy 的构建过程中来做了。

public Factory(long nowMillis, Request request, Response cacheResponse) {
  // 当前发起请求的时间
  this.nowMillis = nowMillis;
  this.request = request;
  this.cacheResponse = cacheResponse;

  // 获取cacheResponse中用于缓存信息的Header和属性,这些值主要是用于计算缓存的存活时间和新鲜时间
  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);
      }
    }
  }
}

这里获取了缓存的一些基本信息,用于后面计算缓存的存活时间和缓存的新鲜时间,用于判断缓存是否过期。

下面根据条件构造 CacheStrategy。

public CacheStrategy get() {
  CacheStrategy candidate = getCandidate();

  if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
    // We're forbidden from using the network and the cache is insufficient.
    return new CacheStrategy(null, null);
  }

  return candidate;
}
private CacheStrategy getCandidate() {
  // 如果没有命中缓存,直接使用网络请求
  if (cacheResponse == null) {
    return new CacheStrategy(request, null);
  }

  // 丢弃缓存,如果缓存缺失三次握手的话(至于什么时候会出现这种情况,并没有深究)
  if (request.isHttps() && cacheResponse.handshake() == null) {
    return new CacheStrategy(request, null);
  }

  // 这里判断Response的Code和header中是否禁用缓存
  if (!isCacheable(cacheResponse, request)) {
    return new CacheStrategy(request, null);
  }

  CacheControl requestCaching = request.cacheControl();
  // noCache 并非不让缓存的意思,它表示请求强制要求执行条件验证;如果请求的Header中包含了”If-Modified-Since”
  // 或“If-None-Match”,同样表示强制条件验证
  if (requestCaching.noCache() || hasConditions(request)) {
    return new CacheStrategy(request, null);
  }

  // 计算缓存的存活时间
  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;
  CacheControl responseCaching = cacheResponse.cacheControl();
  if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
    maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
  }

  // 缓存未过有效期,直接使用缓存
  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\"");
    }
    return new CacheStrategy(null, builder.build());
  }

  // 缓存已过有效期,这里构造一个条件验证的 Request
  String conditionName;
  String conditionValue;
  // 使用 ETag 实体验证
  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;
  // 缓存中不包含用于条件验证的 Header,直接使用网络请求
  } else {
    return new CacheStrategy(request, null); // No condition! Make a regular request.
  }

  Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
  Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);

  Request conditionalRequest = request.newBuilder()
      .headers(conditionalRequestHeaders.build())
      .build();
  return new CacheStrategy(conditionalRequest, cacheResponse);
}

上面就是构造 CacheStrategy 的地方了,可以看到在构造 CacheStrategy 的时候,一共有4种情况,这4种不同的构造方法分别对应了 CacheInterceptor 中对缓存的4种不同处理策略。

  1. new CacheStrategy(null, null):请求中强制使用缓存,但缓存并不存在;
  2. new CacheStrategy(request, null):缓存不存在或不可用,使用网络请求;
  3. new CacheStrategy(null, response):缓存还未过期,直接使用缓存;
  4. new CacheStrategy(request, response):缓存过期,需要进行条件验证。

现在再回过头去看 CacheInterceptor 的 intercept 中的条件判断逻辑,应该就清楚多了。

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

当 Request 中强制要求使用缓存,但缓存并不存在时,构造一个 504 错误。

if (networkRequest == null) {
  return cacheResponse.newBuilder()
      .cacheResponse(stripBody(cacheResponse))
      .build();
}

缓存任然未过期,不需要使用网络请求,直接返回缓存。

Response networkResponse = 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) {
    closeQuietly(cacheCandidate.body());
  }
}

当程序运行到这里时,有两种可能:一是直接发起网络请求,获取原始数据(networkRequest != null && cacheResponse == null);二是需要进行条件验证(networkRequest != null && cacheResponse != null)

如果是第二种情况,则判断验证是否成功。

if (cacheResponse != null) {
  // 如果服务器返回 304 Not Modified,则表示缓存未修改,任然可用,更新缓存的 header;
  // 否则表示缓存过期,服务器会直接返回原始数据
  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();
    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 {
    closeQuietly(cacheResponse.body());
  }
}

最后,如果响应可以被缓存的话,保存缓存。

if (HttpHeaders.hasBody(response)) {
  CacheRequest cacheRequest = maybeCache(response, networkResponse.request(), cache);
  response = cacheWritingResponse(cacheRequest, response);
}

至此,CacheInterceptor 的对缓存的处理流程大致就分析完了,总之这个处理流程也是按照 Http 规范来执行的,具体的缓存处理流程可以参考《HTTP 权威指南》第七章,其中还讲解了如何计算缓存的存活时间、缓存的新鲜时间等。

相关文章

网友评论

    本文标题:OkHttp - Interceptors(二)

    本文链接:https://www.haomeiwen.com/subject/iiehattx.html