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