4、okhttp源码解析-拦截器CacheInterceptor

1、okhttp源码解析-整体流程
2、okhttp源码解析-拦截器RetryAndFllowUpInterceptor
3、okhttp源码解析-拦截器BridgeInterceptor
4、okhttp源码解析-拦截器CacheInterceptor
5、okhttp源码解析-拦截器ConnectInterceptor
6、okhttp源码解析-拦截器CallServerInterceptor
7、okhttp源码解析-Dispatcher任务管理器

一、http 缓存策略

1、强制缓存

2、对比缓存

1、强制缓存

对于强制缓存来说,响应header中会有两个字段来标明失效规则(Expires/Cache-Control)

  • Expires
      Expires的值为服务端返回的到期时间,即下一次请求时,请求时间小于服务端返回的到期时间,直接使用缓存数据。
    不过Expires 是HTTP 1.0的东西,现在默认浏览器均默认使用HTTP 1.1,所以它的作用基本忽略。
    另一个问题是,到期时间是由服务端生成的,但是客户端时间可能跟服务端时间有误差,这就会导致缓存命中的误差。
    所以HTTP 1.1 的版本,使用Cache-Control替代。

  • Cache-Control

    • Cache-Control 是最重要的规则。常见的取值有private、public、no-cache、max-age,no-store,默认为private。
序号 取值 描述
1 private 客户端可以缓存。
2 public 客户端和代理服务器都可缓存(前端的同学,可以认为public和private是一样的)。
3 max-age=xxx 配合强制缓存使用,缓存的内容将在 xxx 秒后失效。
4 no-cache 配合对比缓存使用,需要使用对比缓存来验证缓存数据(需要配合其他header处理,下面是详情)。
5 no-store 不使用缓存,所有内容都不会缓存,强制缓存,对比缓存都不会触发。

举个例子:

image.png

图中Cache-Control仅指定了max-age,所以默认为private,缓存时间为31536000秒(365天)
也就是说,在365天内再次请求这条数据,都会直接获取缓存数据库中的数据,直接使用。

2、对比缓存

对比缓存有两种形式的标识可用。

  • 1、Last-Modified / If-Modified-Since
  • 2、Etag / If-None-Match 这组的优先级大于上一组
2.1、Last-Modified / If-Modified-Since
  • Last-Modified
    服务器在第一次响应时,携带在Response的Header中,告诉浏览器资源的最后修改时间。

  • If-Modified-Since
    客户端发送第二请求时,携带在Request的Header中,值就是last-modified的值。

  • 服务端在第二次收到If-Modified-Since时,会比较当前资源变更的time与if-modified-since时间的先后,判断浏览器是否可以使用缓存数据。如果可以使用缓存就返回304状态码。

2.2、Etag / If-None-Match (优先级大于上一组)
  • Etag
    同样是,服务端在第一次响应时,携带在Response的Header中。其中携带的信息是资源的唯一表示,可以理解成hash值。
  • If-None-Match
    客户端发送第二次请求时,携带在Request的Header中。携带值就是第一次服务器传回的Etag
  • 服务器在接收到If-None-Match时,会和当前资源的唯一值做比较。值相等则说明缓存可用,返回304状态码

二、CacheInterceptor 流程图

上边已经对http的缓存策略有了一些了解,那接下来的拦截器也就清晰了。它其实就是根据http的缓存策略,进行缓存的处理。

  • 1、获取本地的缓存数据cacheResponse
  • 2、通过CacheStrategy类,判断是否满足强制缓存规则
    • 2.1、若满足返回Request为null,则不需要进行网络请求,直接返回缓存数据。
    • 2.2、若不满足,则根据cacheResponse进行Request的Header头If-Modified-Since或者If-None-Match参数封装。
  • 3、通过拦截器责任链,获取网络请求数据networkResponse。chains.process()
  • 4、通过判断网络networkResponse,和本地cacheResponse数据,来判断是否使用缓存。这个过程就是对比缓存规则
    • 4.1 判断是否使用Etag/If-None-Match规则,通过networkResponse的状态码判断是否为304,304即使用缓存。
    • 4.2 判断是否使用Last-Modified/If-Modifed-Since规则,通过判断cacheResponse和netwrokResponse的Last-Modified的值,来判断是否使用缓存。
  • 5、判断此次网络请求的code、Method和Cache-control的值,来决定是否缓存本次数据?
    • 5.1 如果本次请求code是200、203、204等等时,并且cache-control的值不是no-store则说明可以对此次数据进行缓存。
    • 5.2 如果不满足上述条件,则此次数据不缓存,并且要清除历史记录。
CacheInterceptor (1).png

三、源码解析

public Response intercept(Interceptor.Chain chain) throws IOException {
        //1、根据Request信息,获取本地缓存的Response信息。
        Response cacheCandidate = cache != null
                ? cache.get(chain.request())
                : null;

        long now = System.currentTimeMillis();

        //2、根据缓存策略,
        // 2.1处理请求Request信息,
        // 根据缓存的cacheResponse的Header中是否存在Etag/Last-Modified参数,
        // 在Request的header中添加If-None-Match/If-Modified-Since等参数。
        // 2.2获取缓存中的cacheResponse

        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.
        // 3、如果网络请求失败,而且没有缓存信息,直接抛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(EMPTY_BODY)
                    .sentRequestAtMillis(-1L)
                    .receivedResponseAtMillis(System.currentTimeMillis())
                    .build();
        }

        // If we don't need the network, we're done.
        // 4、如果Request经过CacheStrategy策略处理后,是null,那么代表此次请求直接走缓存。一般情况就是命中了强制缓存规则。
        if (networkRequest == null) {
            return cacheResponse.newBuilder()
                    .cacheResponse(stripBody(cacheResponse))
                    .build();
        }

        Response networkResponse = null;
        try {
            // 5、如果走到这,则说明需要发送网络请求。可能是没有缓存,也可能是命中对比缓存规则。
            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) {
        //6、如果缓存cacheResponse不为空,则根据networkResponse返回的状态码,返回true则使用缓存。
            // 6.1、304则使用缓存。这种情况说明使用的Etag/If-None-Match规则,服务器直接判断是否使用缓存,然后返回状态码。
            // 6.2、两次Last-Modified对比之后,发现资源更新时间没有过期,则使用缓存。客户端来判断。
            if (validate(cacheResponse, networkResponse)) {
                Response response = cacheResponse.newBuilder()
                        .headers(combine(cacheResponse.headers(), networkResponse.headers()))
                        .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)) {
            // 7、根据Request信息,判断Http请求是否允许使用缓存。
            CacheRequest cacheRequest = maybeCache(response, networkResponse.request(), cache);
            // 8、根据cacheRequest,存储本次请求的缓存。
            response = cacheWritingResponse(cacheRequest, response);
        }

        return response;
    }

2、validate() 判断对比缓存,是否使用缓存数据。


  /**
   * Returns true if {@code cached} should be used; false if {@code network} response should be
   * used.
   */
  private static boolean validate(Response cached, Response network) {
  //1、如果http返回码是304,则说明使用Etag/If-None-Match规则,直接由服务端判断完,返回给客户端。304则代表使用缓存。
    if (network.code() == HTTP_NOT_MODIFIED) return true;

    // 2、这种则是判断,Last-Modified/If-Modified-Since规则。通过判断两次Response的Last-Modified的时间,来确定是否使用缓存。
    Date lastModified = cached.headers().getDate("Last-Modified");
    if (lastModified != null) {
      Date networkLastModified = network.headers().getDate("Last-Modified");
      if (networkLastModified != null
          && networkLastModified.getTime() < lastModified.getTime()) {
        return true;
      }
    }

    return false;
  }

3、maybeCache() 判断当前http请求是否允许我们使用缓存。允许则进行缓存,不允许则返回null。

  private CacheRequest maybeCache(Response userResponse, Request networkRequest,
      InternalCache responseCache) throws IOException {
      //1.如果缓存工具类为空,则不能进行缓存,则返回null
    if (responseCache == null) return null;

    // Should we cache this response for this request?
    // 2.根据http状态码,以及cache-control的值来判断是否可以进行缓存。例:no-store情况下,不能进行缓存。
    // 这里有个!判断,所以如果进入这个if,则说明不允许进行缓存。
    if (!CacheStrategy.isCacheable(userResponse, networkRequest)) {
        //3.这次请求不允许缓存,但是有可能存在之前的缓存记录。所以要清除历史记录。
        //由于只有"POST"、"PATCH"、"PUT"、"DELETE"、"MOVE"的Method可以缓存,所以清除记录时,也只需要请求这几种状态时的缓存。
      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
        //清除缓存
          responseCache.remove(networkRequest);
        } catch (IOException ignored) {
          // The cache cannot be written.
        }
      }
      return null;
    }

    // Offer this request to the cache.
    // 判断如果走到这个,说明需要缓存,则进行缓存存储
    return responseCache.put(userResponse);
  }
  
  
  //根据http状态码、cache-control值判断此次http请求是否允许缓存。
  public static boolean isCacheable(Response response, Request request) {
    switch (response.code()) {
    //以下这些状态吗,才可使用缓存。但是break之后,仍需判断cache-control的值。
      case HTTP_OK:
      case HTTP_NOT_AUTHORITATIVE:
      case HTTP_NO_CONTENT:
      case HTTP_MULT_CHOICE:
      case HTTP_MOVED_PERM:
      case HTTP_NOT_FOUND:
      case HTTP_BAD_METHOD:
      case HTTP_GONE:
      case HTTP_REQ_TOO_LONG:
      case HTTP_NOT_IMPLEMENTED:
      case StatusLine.HTTP_PERM_REDIRECT:
        break;
      case HTTP_MOVED_TEMP:
      case StatusLine.HTTP_TEMP_REDIRECT:
        if (response.header("Expires") != null
            || response.cacheControl().maxAgeSeconds() != -1
            || response.cacheControl().isPublic()
            || response.cacheControl().isPrivate()) {
          break;
        }
    //其他状态码,直接不能使用缓存。
      default:
        return false;
    }

    // Request、Response的Cache-Control值都不是no-store时才被允许使用缓存。
    return !response.cacheControl().noStore() && !request.cacheControl().noStore();
  }
  
  
  //只有这些http method才能使用缓存
  public static boolean invalidatesCache(String method) {
    return method.equals("POST")
        || method.equals("PATCH")
        || method.equals("PUT")
        || method.equals("DELETE")
        || method.equals("MOVE");     // WebDAV
  }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,997评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,603评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,359评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,309评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,346评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,258评论 1 300
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,122评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,970评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,403评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,596评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,769评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,464评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,075评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,705评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,848评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,831评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,678评论 2 354

推荐阅读更多精彩内容