okhttp3-源码解析(5) CacheInterceptor

okhttp提供了让我们实现缓存的方法和接口,这部分的处理是发生在CaCheInterceptor拦截器中的。
这里说的缓存其实就是把服务器穿回来的响应存储在本地(可以是内存也可以在磁盘),当我们再次准备向服务器发送请求时,就可以直接获取缓存中的响应,而不必再次向服务器发出网络请求。

要完全理解CacheInterceptor的工作机制,其实还是得对Http有清晰的认识。文章最后,也会简单介绍下相关的知识。

1.自定义的缓存

CacheInterceptor的缓存实现是需要我们自己完成的,但是缓存还是需要遵守http规范的。
okhttp提供了 CacheStrategyCacheControl两个类,这两个类会根据http的缓存规则,帮助我们完成实现缓存的一些工作。
CacheStrategy类顾名思义,就是缓存策略。他主要的工作就是根据各种条件判断 是向服务器发送请求 还是 直接从缓存中获取响应,或是会 对 请求报文进行一些设置
CacheControl 类主要解析请求或响应报文中与缓存相关的字段。

有了这两个类的帮助,我们只需要完成 缓存的具体实现,而不需要额外自定义缓存选取的处理逻辑。
下面是InternalCache的接口源码:
可以看到我们自定义缓存时,要实现的方法只有常规的增删改查以及跟踪 缓存相关的请求和响应的两个方法。

public interface InternalCache {
  Response get(Request request) throws IOException;

  CacheRequest put(Response response) throws IOException;

  /**
   * Remove any cache entries for the supplied {@code request}. This is invoked when the client
   * invalidates the cache, such as when making POST requests.
   */
  void remove(Request request) throws IOException;

  /**
   * Handles a conditional request hit by updating the stored cache response with the headers from
   * {@code network}. The cached response body is not updated. If the stored response has changed
   * since {@code cached} was returned, this does nothing.
   */
  void update(Response cached, Response network);

  /** Track an conditional GET that was satisfied by this cache. */
  void trackConditionalCacheHit();

  /** Track an HTTP response being satisfied with {@code cacheStrategy}. */
  void trackResponse(CacheStrategy cacheStrategy);
}

2.CacheInterceptor的工作流程

先来看CacheInterceptor的具体工作逻辑,下面是他的定义的变量以及Interceptor方法的源码:

  final InternalCache cache;//自定义的缓存

  public CacheInterceptor(InternalCache cache) {//通过构造方法传入
    this.cache = cache;
  }

//下面是intercept方法
@Override public Response intercept(Chain chain) throws IOException {
  Response cacheCandidate = cache != null
      ? cache.get(chain.request())//如果实现了内部缓存,那么就从内部缓存中取
      : null;//如果没有,那么就为null

  long now = System.currentTimeMillis();//获取目前的时间,后面要用于计算

  CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
  Request  = strategy.networkRequest;
  Response cacheResponse = strategy.cacheResponse;
 //根据返回的networkRequest和cacheResponse
    //进行处理。总的来说,如果值为null那就不操作
    0 
  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.
  // 缓存响应无法获取(可能过期了,可能根本就没有缓存),但请求又要求了only-if-cached,只能返回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();
  }

  // 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) {
     //网络响应返回null(失败了),缓存响应不为null时,清空缓存
      closeQuietly(cacheCandidate.body());
    }
  }

  // If we have a cache response too, then we're doing a conditional get.
 //如果我们有本地缓存响应,那么还会获取本地缓存响应的某些属性,然后进行设置
 //ps:Response 类对象中会持有  网络响应(NetWorkResponse) 和缓存响应(CaCheResponse) 
  if (cacheResponse != null) {
    if (networkResponse.code() == HTTP_NOT_MODIFIED) {//响应码是304,需要用缓存响应
      Response response = cacheResponse.newBuilder()//合并首部行,设置Response
          .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()).
        //机翻: 在合并标头之后但在剥离 Content-Encoding 标头之前更新缓存(由                 // initContentStream() 执行)。
        //更新本地缓存。之后响应会被传回BridgeInterceptor,那里会改变首部行
      cache.trackConditionalCacheHit();
      cache.update(cacheResponse, response);
      return response;//返回给上一级拦截器(BridgeInterceptor)
    } else {
        //不是304的情况
      closeQuietly(cacheResponse.body());
    }
  }

  Response response = networkResponse.newBuilder()//和上面的差不多,设置response
      .cacheResponse(stripBody(cacheResponse))
      .networkResponse(stripBody(networkResponse))
      .build();

  if (cache != null) {//回忆一下,cache是自定义的本地缓存
    if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {//判断服务器返回的响应,能否在本地缓存
      // Offer this request to the cache.
        //把响应交给自定义的缓存,最后返回
      CacheRequest cacheRequest = cache.put(response);
      return cacheWritingResponse(cacheRequest, response);
    }
	
      //HttpMethod.invalidatesCache方法判断该响应可以缓存
      //结果却没有缓存,那么肯定是自定义的缓存有问题
    if (HttpMethod.invalidatesCache(networkRequest.method())) {
      try {
        cache.remove(networkRequest);
      } catch (IOException ignored) {
        // The cache cannot be written.
      }
    }
  }

  return response;
}

Intercetor的大体流程如下:
分别将从责任链中取到的request和从缓存中去到的response分别赋给Networkrequest和Cacheresponse。
之后将Networkrequest和Cacheresponse。传入CacheStartay的Factory中。在build()方法里,会读取解析对应的请求报文和响应报文。并根据Http缓存的相关规则,对 Networkrequest和Cacheresponse进行设置。完成设置后,在Interceptor方法中决定是否从缓存中读取。

3.CacheStartay的get()方法

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

在Interceptpr方法中有上面这一段代码,通过调用CacheStrategy的Factory和get方法,我们获取
到networkRequest和cacheResponse并以此为根据,进行请求和获取缓存工作。
下面我们就来看一看这个两个方法的具体实现,了解okhttp具体的缓存存取策略

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

private CacheStrategy getCandidate() {
      // No cached response.
      //没有设置缓存,直接返回
      if (cacheResponse == null) {
        return new CacheStrategy(request, null);
      }

      // Drop the cached response if it's missing a required handshake.
    //如果请求是https,但缓存响应缺少TLS握手,那么缓存也拜拜
      if (request.isHttps() && cacheResponse.handshake() == null) {
        return new 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)) {//检查缓存响应,是否ok
        return new CacheStrategy(request, null);
      }

      CacheControl requestCaching = request.cacheControl();
    //CacheControl 可以理解为也是一个缓存判断相关的类,判断的是http缓存
    //而CacheStrategy负责与本地缓存打交道
      if (requestCaching.noCache() || hasConditions(request)) {
          //请求不需要缓存或者请求是有条件get请求(不拿本地缓存,要问服务器)的情况
        return new CacheStrategy(request, null);
      }

      CacheControl responseCaching = cacheResponse.cacheControl();
    //和上面不同,这个是缓存响应调用

      long ageMillis = cacheResponseAge();//缓存响应的实际age
      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());
      }//只接收minFreshMills时间内未过期的缓存(从现在开始计算)

      long maxStaleMillis = 0;
      if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
        maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
      }//过期后的时间不超过  maxStaleMillis 的话,缓存还能用
		
		//下面就是判断缓存是否过期的相关计算
      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());
      }

      // Find a condition to add to the request. If the condition is satisfied, the response body
      // will not be transmitted.
    //寻找协商缓存的相关字段并设置到请求首部行中,并移除requestBody(有条件Get请求的规定)
      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); // 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);
    }

4. 总结

再详细查看CacheInterceptor的源码之后,你会发现CacheInterceptor的工作流程其实并不复杂,总的来说,他只是根据http中关于缓存的相关规则 来做对应的处理。
下面是总结的几个场景,如果对http缓存的知识有一定的了解的话,这部分应该很好理解。

请求场景处理结果
没有自定义缓存(cache 为 null),且请求没有指明只用缓存将请求传递给下一个拦截器以进行网络请求
缓存不可用或者根本没定义缓存,但请求指明only-if-cached(只用缓存)构建一个响应码为504的http响应并返回
有缓存,但请求是有条件GET请求将请求传递给下一个拦截器以进行网络请求,并根据服务器返回的响应码决定是否选择缓存响应
有缓存,且缓存未过期直接返回缓存响应,不进行网络请求
缓存过期,且无协商缓存相关首部字段将请求传递给下一个拦截器以进行网络请求
缓存过期,但有协商缓存相关首部字段对请求进行设置后,递给下一个拦截器以进行网络请求,并根据服务器返回的响应码决定是否选择缓存响应

5.Http缓存规则简单介绍

Http缓存大体可以分为强缓存和协商缓存。
强缓存的含义简单来说其实就是 服务器在相应报文中添加max-age字段,以标明缓存过期时间。没超过过期时间的资源,客户端就可以直接获取。
而协商缓存的意识就是客户端在获取缓存的时候,还需要根据缓存中的相关字段向服务器发出询问。如果收到服务器发来的响应码为304的报文,才能够使用缓存。
这里需要注意:强缓存的优先级是高于协商缓存的。

下面总结了CacheInterceptor的实现中,出现的一些缓存相关首部字段。

响应报文标识含义
Last-Modified资源的最后修改时间
Expires缓存有效时间,超过这个时间,缓存无效(这个单位是时间戳)
ETag资源标识码,ETag改变说明资源已改变
max-age缓存可用的最大时间(单位是时间长度)请求报文中也可以有
请求报文标识含义
If-Modified-Since之前缓存中的Last-Modified值
If-None-Match之前缓存中的资源标识码
min-fresh希望收到 该时间长度内没有过期的缓存(从发出请求开始计时)
max-stale即使过期了 一定的时间的缓存,也可以用(单位也是时间长度)
only-if-cache只要缓存中的资源。如果缓存过期,一般会返回504

由于水平有限,有关http缓存的这部分知识可能有错误。想要详细了解的话,可以查阅相关的博客或者文章。
上面也只是记录一些缓存相关的常用字段,具体可以查看RFC规范
sintall.com/RFC - 码云 - 开源中国 (gitee.com)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值