OKHttp3--缓存拦截器CacheInterceptor源码解析【八】

本文深入解析OKHttp的缓存机制,包括缓存拦截器CacheInterceptor的工作原理,缓存策略CacheStrategy的决策过程,以及如何判断响应是否可缓存。文章详细介绍了缓存的获取、缓存策略的创建、缓存的使用条件,以及如何处理网络请求和缓存的交互。

系列

OKHttp3–详细使用及源码分析系列之初步介绍【一】
OKHttp3–流程分析 核心类介绍 同步异步请求源码分析【二】
OKHttp3–Dispatcher分发器源码解析【三】
OKHttp3–调用对象RealCall源码解析【四】
OKHttp3–拦截器链RealInterceptorChain源码解析【五】
OKHttp3–重试及重定向拦截器RetryAndFollowUpInterceptor源码解析【六】
OKHttp3–桥接拦截器BridgeInterceptor源码解析及相关http请求头字段解析【七】
OKHttp3–缓存拦截器CacheInterceptor源码解析【八】
OKHttp3-- HTTP缓存机制解析 缓存处理类Cache和缓存策略类CacheStrategy源码分析 【九】

CacheInterceptor

作为拦截器链中的第三个拦截器,缓存拦截器充当着节约资源大使的角色,替用户充分考虑,有缓存时使用缓存,没有再使用网络请求,然后将响应缓存起来

继续上一篇文章的节奏,我们依然从构造方法开始看

构造方法

  final InternalCache cache;

  public CacheInterceptor(InternalCache cache) {
    this.cache = cache;
  }

它的实例化是在RealCall当中

interceptors.add(new CacheInterceptor(client.internalCache()));

从这里可以看到参数是需要我们在构建OKHttpClient的时候传入的,如果没有传,那OKHttp是默认不会使用缓存的

InternalCache它是一个接口,但是OKHttp不推荐开发者自定义该接口的实现类,而应该使用OKHttp提供的实现类Cache

public interface InternalCache {
  // 根据请求获取响应
  Response get(Request request) throws IOException;
 // 存储网络响应 并返回缓存的请求
  CacheRequest put(Response response) throws IOException;
  // 当缓存无效的时候移除缓存 
  // 比如OKHttp只支持缓存get请求,其它方法的响应不缓存
  void remove(Request request) throws IOException;
  
  void update(Response cached, Response network);

  void trackConditionalCacheHit();

  void trackResponse(CacheStrategy cacheStrategy);
}

所以我们在构建OKHttpClient的时候正确做法如下

httpClient = new OkHttpClient
                .Builder()
                .cache(new Cache(new File("cache"),1024*1024*24))
                .build();

Cache类接收两个参数,第一个参数是缓存目录,第二个参数是最大缓存值

intercept

接着看它的拦截方法

  @Override 
  public Response intercept(Chain chain) throws IOException {
    // 通过request从缓存中获取响应
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();
    /**
    * CacheStrategy 是一个缓存策略类 比如强制缓存 对比缓存等 它决定是使用缓存还是进行网络请求
    * 其内部维护了Request、Response
    * 如果Request为null表示不使用网络
    * 如果Response为null表示不使用缓存
    */
    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.
    }

    // 如果缓存策略是不使用网络也不使用缓存,那就构建一个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 (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }

    // 定义响应
    Response networkResponse = null;
    try {
     // 到这里就说明可以使用网络,那就交给下一个拦截器去处理获取响应
      networkResponse = chain.proceed(networkRequest);
    } finally {
      // 如果发生了IO异常或者其它异常,关闭缓存避免内存泄漏
      if (networkResponse == null && cacheCandidate != null) {
        closeQuietly(cacheCandidate.body());
      }
    }

    // 如果缓存策略是可以使用缓存
    if (cacheResponse != null) {
      // 且网络响应码是304 HTTP_NOT_MODIFIED说明本地缓存可以使用
      // 且网络响应是没有响应体的
      // 这时候就合并缓存响应和网络响应并构建新的响应
      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();

        // 在合并标头之后但在剥离Content-Encoding标头之前更新缓存
        cache.trackConditionalCacheHit();
        cache.update(cacheResponse, response);
        return response;
      } else {
        closeQuietly(cacheResponse.body());
      }
    }

    // 走到这里说明缓存策略是不可以使用缓存或本地缓存不可用
    // 那就通过网络响应构建响应对象
    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

   // 客户端允许使用缓存
    if (cache != null) {
      // 如果响应有响应体且响应可以缓存 那就将响应写入到缓存
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        // 缓存响应的部分信息
        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.
        }
      }
    }

    return response;
  }

可以看到想要缓存一个正确的网络响应,需要满足三个条件

  • 客户端允许缓存,即设置了Cache
  • 响应有响应体
  • 响应是符合缓存条件的

第一个条件我们默认设置了,要不然就没得说了;第二个条件是通过hasBody方法判断的

HttpHeaders.hasBody

  // 判断网络响应是否有响应体
  public static boolean hasBody(Response response) {
    // 如果请求方法是HEAD,直接return
    // 该方法只有响应头,没有响应体
    if (response.request().method().equals("HEAD")) {
      return false;
    }

    int responseCode = response.code();
    //根据响应码判断,如果符合条件就返回true,认为有响应体
    if ((responseCode < HTTP_CONTINUE || responseCode >= 200)
        && responseCode != HTTP_NO_CONTENT
        && responseCode != HTTP_NOT_MODIFIED) {
      return true;
    }

    // 如果不符合上述响应码,我们还是要看下Content-Length和Transfer-Encoding响应头的情况来决定是否缓存
    // Content-Length:这是表示响应体的长度,contentLength(response)也就是获取该字段的值,如果大于0那就肯定有响应体
    // Transfer-Encoding:分块传输 就是将响应体分成多个块进行传输,这个也就代表着肯定有响应体
    if (contentLength(response) != -1
        || "chunked".equalsIgnoreCase(response.header("Transfer-Encoding"))) {
      return true;
    }

    return false;
  }

响应码

  • 1XX: 100-199 的状态码代表着信息性状态码,比如上面的HTTP_CONTINUE(100)

  • 2xx: 200-299 的状态码代表着成功状态码,但是204状态码代表着没有实体仅仅有响应行和响应头,HTTP_NO_CONTENT即是204

  • 3xx: 300-399 的状态码代表着重定向 但是304状态码代表着请求资源未修改,请使用缓存,只有响应行和响应头没有响应体,即HTTP_NOT_MODIFIED

  • 4xx: 400-499 的状态码代表着客户端错误代码

  • 5xx: 500-599 的状态码代表着服务器错误代码

上面的通过响应码的判断逻辑也就是当状态码小于100的时候,那肯定也就小于204和304,是有响应体的
当状态码大于等于200的时候,同时不等于204和304,也说明有响应体,需要进行缓存网络响应

这里讲下304这个响应码,即HTTP_NOT_MODIFIED对应的值:

  • 当客户端第一次访问服务器,服务器在返回资源的同时会将一个ETag值保存在响应头,ETag值表示当前请求的资源在服务器的唯一标识,生成规则由服务器决定;客户端接收到后会缓存响应中的数据和ETag值
  • 当客户端发现该资源过期时,如果它还有ETag声明,那再次向服务器请求时会带上If-None-Match请求头,值就是ETag的值
  • 服务器接收到请求后,发现请求头有If-None-Match值,那就跟该资源的ETag值进行比较,根据结果返回200或者304

更多关于HTTP响应码可以参考HTTP响应码

CacheStrategy.isCacheable

第三个条件就是判断响应是否符合缓存条件

  public static boolean isCacheable(Response response, Request request) {
    // Always go to network for uncacheable response codes (RFC 7231 section 6.1),
    // This implementation doesn't support caching partial content.
    switch (response.code()) {
      //200 成功返回状态码
      case HTTP_OK:
      //203 实体头部包含的信息来自于资源源服务器的副本而不是来自于源服务器
      case HTTP_NOT_AUTHORITATIVE:
      //204 只有头部没有实体,在根据响应码判断是否有响应体那时已经排除了204 和304的状态码
      case HTTP_NO_CONTENT:
      case HTTP_MULT_CHOICE:
      case HTTP_MOVED_PERM:
      //404 找不到资源
      case HTTP_NOT_FOUND:
      //405 请求方法不支持
      case HTTP_BAD_METHOD:
      //410 和404类似 只是服务器以前拥有该资源但是删除了
      case HTTP_GONE:
      //414 客户端发出的url长度太长
      case HTTP_REQ_TOO_LONG:
      //501 服务器发生一个错误 无法提供服务
      case HTTP_NOT_IMPLEMENTED:
      case StatusLine.HTTP_PERM_REDIRECT:
        // These codes can be cached unless headers forbid it.
        break;
      //302 请求的url已被移除
      case HTTP_MOVED_TEMP:
      //307 和302类似
      case StatusLine.HTTP_TEMP_REDIRECT:
        // 当拥有正确的头部时,这些状态码都表示可缓存
        if (response.header("Expires") != null
            || response.cacheControl().maxAgeSeconds() != -1
            || response.cacheControl().isPublic()
            || response.cacheControl().isPrivate()) {
          break;
        }
      default:
        // 其它状态吗都不给予缓存
        return false;
    }
    // A 'no-store' directive on request or response prevents the response from being cached.
    return !response.cacheControl().noStore() && !request.cacheControl().noStore();
  }

HttpMethod.invalidatesCache

最后再看下对请求方法的判断

public static boolean invalidatesCache(String method) {
	return method.equals("POST")
	    || method.equals("PATCH")
	    || method.equals("PUT")
	    || method.equals("DELETE")
	    || method.equals("MOVE");
}

可以看到我们的网络请求方法是POST,PATCH,PUT,DELETE,MOVE这些方法的时候,会将对应的缓存删除

同时在hasBody方法中还屏蔽了HEAD方法,所以这里会产生一条结论就是OKHttp支持缓存的网络请求方法只有get方法了

总结

从上面的方法总结下缓存拦截器的工作逻辑

  • 通过request从缓存中获取响应
  • 创建缓存策略,从缓存策略类中获取Request和Response
  • 如果Request和Response都为null,说明网络不可用且缓存也不可用,那就构建504响应返回
  • 如果Request为null,说明网络不可用,但是可以使用缓存,那就通过缓存策略获取到的缓存构建响应返回
  • 如果网络可以使用,那就通过下一个拦截器获取响应
  • 如果缓存策略获取到的缓存可以用,同时网络请求获取到的响应码是304,说明服务器没有修改数据,本地缓存是可以用的,那就通过这两个响应构建新的响应并返回
  • 走到这里说明缓存策略中的缓存不能使用,即服务器修改了数据,那就通过网络响应构建新的响应
  • 如果客户端允许缓存,且响应有响应体,同时响应可以缓存 那就将响应写入到缓存
  • 再通过请求方法判断是否需要缓存,不需要的话就删除
  • 将最新数据返回给上一个拦截器

总体看下来缓存拦截器并不难理解,但是其中涉及到两个非常重要的类

  • Cache:OKHttp中缓存具体的操作类
  • CacheStrategy:缓存策略类,由它决定是使用缓存还是使用网络请求

这两个类将在接下来的文章中进行解析

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值