美文网首页CDN框架Code
OKHttp源码解析(六)--中阶之缓存基础

OKHttp源码解析(六)--中阶之缓存基础

作者: 隔壁老李头 | 来源:发表于2017-06-04 12:45 被阅读1470次

    前面一节课主要讲解了interceptor及其调用链,本篇这主要讲解http的缓存处理,大体流程如下:

    1.什么是缓存
    2.为什么要用缓存
    3.HTTP缓存机制
    4.CacheControl类详解
    5.CacheStrategy类详解
    6.CacheInterceptor类详解

    一、Cache缓存的简介

    缓存,顾名思义,也就是方便用户快速获取值的一种存储方式。小到CPU同频的昂贵的缓存颗粒,内缓存,硬盘,网络,CDN反缓存,DNS递归查询,OS页面置换,Redis数据库,都可以看作缓存。它有如下的特点:

    • 1.缓存载体与持久载体总是相对的,体量远远小于持久载体,成本高于之久体量,但是速度却高速持久体量。
    • 2.需要实现排序依据,比如在java中可以使用Comparable<T>作为排序的接口
    • 3.需要一种页面置换算法(page replacement algorithm)将旧页面去掉换成新页面,如最久未使用算法(LRU)、先进先出算法(FIFO)、最紧最小使用算法(LFU)、非最紧使用算法(NMRU)等
    • 4.可溯源,如果没有命中缓存,就需要从原始地址获取,这个步骤叫做"回源头",CDN厂商会标注"回源率"作为卖点

    PS:在OKHTTP中,使用FileSystem作为缓存载体(磁盘相对于网络缓存),使用LRU作为页面置换算法(封装了LinkedHashMap)。

    HTTP作为客户端与服务器沟通的重要协议,对从事android开发的同学来说是一个非常重要的环节,其中网络层优化又是重中之重。今天主要是讲解OKHTTP中的缓存处理,那么首先先简单介绍下为什么要用缓存

    二、为什么要用缓存

      缓存对移动端非常重要,使用缓存可以提高用户体验,用缓存的主要在于:
    
    • 1 减少请求次数,较少服务器压力
    • 2本地数据读取更快,让页面不会空白几百毫秒
    • 3在无网络的情况下提供数据
      HTTP缓存是最好的减少客户端服务器往返次数的方案,缓存提供了一种机制来保证客户端或者代理能够存储一些东西,而这些东西将会在稍后的HTTP响应中用到的。(即第一次请求了,到了客户端,缓存起来,下次如果请求还需要一些资源,就不用到服务器去取了)这样,就不用让一些资源再次跨越整个网络了。


      请求与缓存.png

    三、HTTP缓存机制

    1、HTTP报文

    HTTP报文就是客户端和服务器之间通信时发送及其响应的数据块。客户端向服务器请求数据,发送请求(request)报文;服务器向客户端下发返回数据,返回响应(response)报文,报文信息主要分为两部分。

    • 1包含属性的头部(header)-------------附加信息(cookie,缓存信息等),与缓存相关的规则信息,均包含在header中
    • 2包含数据的主体部分(body)--------------HTTP请求真正想要传输的部分

    2、缓存分类

    (1)按照"端“”分类

    缓存可以分为

    • 1、服务器缓存,其中服务器缓存又可以分为服务器缓存和反向代理服务器缓存(也叫网关缓存,比如Nginx反向代理,Squid等),其实广泛使用的CSN也是一种服务端缓存,目的都是让用户的请求走"捷径",并且都是缓存图片、文件等静态资源。
    • 2、客户端缓存
      客户端缓存则一般是只浏览器缓存,目的就是加速各种静态资源的访问,想想淘宝,京东,百度随便一个网页都是上百请求,每天PV都是上亿的,如果没有缓存,用户体验会急剧下降,同时服务器压力巨大。
    (2) 按照"是否想服务器发起请求,进行对比"分类

    可以分为:

    • 1 强制缓存(不对比缓存)
    • 2 对比缓存

    已存在缓存数据时,仅基于强制缓存,请求数据流程如下:

    强制缓存.png

    已存在缓存数据时,仅基于对比缓存,请求数据的流程如下:


    对比缓存.png

    我们可以看到两类缓存规则的不同,强制缓存如果生效,则不再和服务器交互了,而对比缓存不惯是否生效,都需要和服务器发生交互。
    通过上面了解到,在缓存数据未失效的情况下,可以直接使用缓存数据,那么客户端是怎么判断数据是否失效的?同理,什么时候采用强制缓存,而什么时候又采用对比缓存,这里面客户端是怎么和服务器进行交互的?上面也说道,缓存规则是包含在响应header里面的。莫非所有的交互在header里面?

    3、请求头header中有关缓存的设置

    3.1 expires

    在HTTP/1.0中expires的值围服务器端返回的到期时间,即下一次请求时,请求时间小于服务器返回的到期时间,直接使用缓存数据,这里面有个问题,由于到期时间是服务器生成的,但是客户端的时间可能和服务器有误差,所以这就会导致误差,所以到了HTTP1.1基本上不适用expires了,使用Cache-Control替代了expires

    3.2 Cache-Control

    Cache-Control 是最重要的规则。常见的取值有private、public
    、no-cache、max-age、no-store、默认是private。

    响应头部 意义
    Cache-Control:public 响应被公有缓存,移动端无用

    响应头部 意义
    Cache-Control:public 响应被共有缓存,移动端无用
    Cache-Control:private 响应被私有缓存,移动端无用
    Cache-Control:no-cache 不缓存
    Cache-Control:no-store 不缓存
    Cache-Control:max-age=60 60秒之后缓存过期

    (PS:在浏览器里面,private 表示客户端可以缓存,public表示客户端和服务器都可以缓存)
    举个例子。入下图:


    缓存header.png

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

    3.3 Last-Modified/If-Modified-Since

    上面提到了对比缓存,顾名思义,需要进行比较判断是否可以使用缓存,客户端第一次发起请求时,服务器会将缓存标志和数据一起返回给客户端,客户端当二者缓存至缓存数据库中。再次其你去数据时,客户端将备份的缓存标志发送给服务器,服务器根据标志来进行判断,判断成功后,返回304状态码,通知客户端比较成功,可以使用缓存数据。
    上面说到了对比缓存的流程,那么具体又是怎么实现的那?

    Last-Modified

    是通过Last-Modified/If-Modified-Since来实现的,服务器在响应请求时,告诉浏览器资源的最后修改时间。

    Last-Modified.png
    If-Modified-Since

    再次请求服务器时,通过此字段通知服务器上次请求时,服务器返回最远的最后修改时间。服务器收到请求后发现有If-Modified-Since则与被请求资源的最后修改时间进行对比。若资源的最后修改时间大于If-Modified-Since,说明资源又被改动过,则响应整个内容,返回状态码是200.如果资源的最后修改时间小于或者等于If-Modified-Since,说明资源没有修改,则响应状态码为304,告诉客户端继续使用cache.

    If-Modified-Since.png
    3.4 ETag/If-None-Match(优先级高于Last-Modified/If-Modified-Since)

    Etag:
    服务响应请求时,告诉客户端当前资源在服务器的唯一标识(生成规则由服务器决定)


    Etag.png

    If-None-Match:
    再次请求服务器时,通过此字段通知服务器客户端缓存数据的唯一标识。服务器收到请求后发现有头部If-None-Match则与被请求的资源的唯一标识进行对比,不同则说明资源被改过,则响应整个内容,返回状态码是200,相同则说明资源没有被改动过,则响应状态码304,告知客户端可以使用缓存


    If-None-Match.png

    正式使用时按需求也许只包含其中部分字段,客户端要根据这些信息存储这次请求信息,然后在客户端发起的时间内检查缓存,遵循下面的步骤

    缓存.png

    四.Cache-Control类详解

    CacheControl 对应HTTP里面的CacheControl

    public final class CacheControl {
    
      private final boolean noCache;
      private final boolean noStore;
      private final int maxAgeSeconds;
      private final int sMaxAgeSeconds;
      private final boolean isPrivate;
      private final boolean isPublic;
      private final boolean mustRevalidate;
      private final int maxStaleSeconds;
      private final int minFreshSeconds;
      private final boolean onlyIfCached;
      private final boolean noTransform;
    
      /**
       * Cache control request directives that require network validation of responses. Note that such
       * requests may be assisted by the cache via conditional GET requests.
       */
      public static final CacheControl FORCE_NETWORK = new Builder().noCache().build();
    
      /**
       * Cache control request directives that uses the cache only, even if the cached response is
       * stale. If the response isn't available in the cache or requires server validation, the call
       * will fail with a {@code 504 Unsatisfiable Request}.
       */
      public static final CacheControl FORCE_CACHE = new Builder()
          .onlyIfCached()
          .maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS)
          .build();
    }
    

    CacheControl类是对HTTP的Cache-Control头部的描述。CacheControl没有公共的构造方法,内部通过一个Build进行设置值,获取值可以通过CacheControl对象进行获取。
    Builder具体有如下设置方法:

    • 1、noCache()
      对应于“no-cache”,如果出现在 响应 的头部,不是表示不允许对响应进行缓存,而是表示客户端需要与服务器进行再次验证,进行一个额外的GET请求得到最新的响应;如果出现请求头部,则表示不适用缓存响应,即记性网络请求获取响应。
    • 2、noStore()
      对应于"no-store",如果出现在响应头部,则表明该响应不能被缓存
    • 3、maxAge(int maxAge,TimeUnit timeUnit)
      对应"max-age",设置缓存响应的最大存货时间。如果缓存响满足了到了最大存活时间,那么将不会再进行网络请求
    • 4、maxStale(int maxStale,TimeUnit timeUnit)
      对应“max-stale”,缓存响应可以接受的最大过期时间,如果没有指定该参数,那么过期缓存响应将不会被使用
    • 5、minFresh(int minFresh,TimeUnit timeUnit)
      对应"min-fresh",设置一个响应将会持续刷新最小秒数,如果一个响应当minFresh过去后过期了,那么缓存响应不能被使用,需要重新进行网络请求
    • 6、onlyIfCached()
      对应“onlyIfCached”,用于请求头部,表明该请求只接受缓存中的响应。如果缓存中没有响应,那么返回一个状态码为504的响应。

    CacheControl类中还有其他方法,这里就不一一介绍了。想了解的可以去API文档查看。
    对于常用的缓存控制,CacheControl中提供了两个常用的于修饰请求,FORCE_CACHE表示只使用缓存中的响应,哪怕这个缓存过期了,FORCE_NETWORK这个表示只能使用网络响应

    五、CacheStrategy类详解

    CacheStrategy 缓存策略类
    OKHTTP使用了CacheStrategy实现了上面的流程图,它根据之前缓存的结果与当前将要发送Request的header进行策略,并得出是否进行请求的结果。

    (一)、策略原理

    根据输出的networkRequest和cacheResponse的值是否为null给出不同的策略,如下:

    networkRequest cacheResponse result 结果
    null null only-if-cached (表明不进行网络请求,且缓存不存在或者过期,一定会返回503错误)
    null non-null 不进行网络请求,直接返回缓存,不请求网络
    non-null null 需要进行网络请求,而且缓存不存在或者过去,直接访问网络
    non-null non-null Header中包含ETag/Last-Modified标签,需要在满足条件下请求,还是需要访问网络

    以上是对networkRequest/cacheResponse进行的switch查询得出的,下面我们就详细讲解下

    (二)、CacheStrategy类的构造

    CacheStrategy使用Factory模式进行构造,通过Factory的get()方法获取CacheStrategy的对象,参数如下:

        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();
            //获取cacheReposne中的header中值
            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);
              }
            }
          }
        }
    
        /**
         * Returns a strategy to satisfy {@code request} using the a cached response {@code response}.
         */
        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;
        }
        /**
         * Returns a strategy to satisfy {@code request} using the a cached response {@code response}.
         */
        public CacheStrategy get() {
          //获取当前的缓存策略
          CacheStrategy candidate = getCandidate();
         //如果是网络请求不为null并且请求里面的cacheControl是只用缓存
          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;
        }
    
        /** Returns a strategy to use assuming the request can use the network. */
        private CacheStrategy getCandidate() {
          // No cached response.
          //如果没有缓存响应,返回一个没有响应的策略
          if (cacheResponse == null) {
            return new CacheStrategy(request, null);
          }
           //如果是https,丢失了握手,返回一个没有响应的策略
          // Drop the cached response if it's missing a required handshake.
          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)) {
            return new CacheStrategy(request, null);
          }
         
          //获取请求头里面的CacheControl
          CacheControl requestCaching = request.cacheControl();
          //如果请求里面设置了不缓存,则不缓存
          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());
          }
         //响应支持缓存
           //持续时间+最短刷新时间<上次刷新时间+最大验证时间 则可以缓存
          //现在时间(now)-已经过去的时间(sent)+可以存活的时间<最大存活时间(max-age)
          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,必须要满足一定的条件
          // Find a condition to add to the request. If the condition is satisfied, the response body
          // will not be transmitted.
          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 {
            //没有条件则返回一个定期的request
            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();
          //返回有条件的缓存request策略
          return new CacheStrategy(conditionalRequest, cacheResponse);
        }
    

    通过上面分析,我们可以发现,OKHTTP实现的缓存策略实质上就是大量的if/else判断,这些其实都是和RFC标准文档里面写死的。
    上面说了这么多,那么咱们要开始今天的主题了----CacheInterceptor类

    六、 CacheInterceptor 类详解

    BridgeInterceptor :负责将请求返回 关联的保存到缓存中。客户端和服务器根据一定的机制(策略CacheStrategy ),在需要的时候使用缓存的数据作为网络响应,节省了时间和宽带。

    老规矩上源码:

      //CacheInterceptor.java
     @Override 
     public Response intercept(Chain chain) throws IOException {
        //如果存在缓存,则从缓存中取出,有可能为null
        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);
        }
        //缓存策略不为null并且缓存响应是null
        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 (cache != null) {
           //缓存到本地
          if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
            // Offer this request to the cache.
            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;
      }
    

    简单的说下上述流程:
    1、如果配置缓存,则从缓存中取一次,不保证存在
    2、缓存策略
    3、缓存监测
    4、禁止使用网络(根据缓存策略),缓存又无效,直接返回
    5、缓存有效,不使用网络
    6、缓存无效,执行下一个拦截器
    7、本地有缓存,根具条件选择使用哪个响应
    8、使用网络响应
    9、 缓存到本地
    大体流程分析完,那么咱们再详细分析下。
    首先说到了缓存就不得不提下OKHttp里面的Cache.java类和InternalCache.java那么咱们就简单的聊下这两个类

    (一)、Cache.java类

    Cache

    1、基本特征

    private final DiskLruCache cache; 
    
    public Cache(File directory, long maxSize) {  
        this(directory, maxSize, FileSystem.SYSTEM);  
      }  
    Cache(File directory, long maxSize, FileSystem fileSystem) {  
        this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize);  
    }  
    

    通过上面代码可知

    • 1、Cache对象拥有一个DiskLruCache引用。
    • 2、Cache构造器接受两个参数,意味着如果我们想要创建一个缓存必须指定缓存文件存储的目录和缓存文件的最大值

    2、既然是Cache,那么一定会有"增"、"删"、"改"、"查"。

    (1) ”增“操作——put()方法
    CacheRequest put(Response response) {
        String requestMethod = response.request().method();
        //判断请求如果是"POST"、"PATCH"、"PUT"、"DELETE"、"MOVE"中的任何一个则调用DiskLruCache.remove(urlToKey(request));将这个请求从缓存中移除出去。
        if (HttpMethod.invalidatesCache(response.request().method())) {
          try {
            remove(response.request());
          } catch (IOException ignored) {
            // The cache cannot be written.
          }
          return null;
        }
        //判断请求如果不是Get则不进行缓存,直接返回null。官方给的解释是缓存get方法得到的Response效率高,其它方法的Response没有缓存效率低。通常通过get方法获取到的数据都是固定不变的的,因此缓存效率自然就高了。其它方法会根据请求报文参数的不同得到不同的Response,因此缓存效率自然而然就低了。
        if (!requestMethod.equals("GET")) {
          // Don't cache non-GET responses. We're technically allowed to cache
          // HEAD requests and some POST requests, but the complexity of doing
          // so is high and the benefit is low.
          return null;
        }
         //判断请求中的http数据包中headers是否有符号"*"的通配符,有则不缓存直接返回null
        if (HttpHeaders.hasVaryAll(response)) {
          return null;
        }
        //由Response对象构建一个Entry对象,Entry是Cache的一个内部类
        Entry entry = new Entry(response);
        //通过调用DiskLruCache.edit();方法得到一个DiskLruCache.Editor对象。
        DiskLruCache.Editor editor = null;
        try {
          editor = cache.edit(key(response.request().url()));
          if (editor == null) {
            return null;
          }
          //把这个entry写入
          //方法内部是通过Okio.buffer(editor.newSink(ENTRY_METADATA));获取到一个BufferedSink对象,随后将Entry中存储的Http报头数据写入到sink流中。
          entry.writeTo(editor);
          //构建一个CacheRequestImpl对象,构造器中通过editor.newSink(ENTRY_BODY)方法获得Sink对象
          return new CacheRequestImpl(editor);
        } catch (IOException e) {
          abortQuietly(editor);
          return null;
        }
      }
    
    

    总结一下上述步骤
    第一步,先判断是不是一个正常的请求(get,post等)
    第二步,由于只支持get请求,非get请求直接返回
    第三步,通配符过滤
    第四步,通过上述检验后开始真正的缓存流程,new一个Entry
    第五步,获取一个DiskLruCache.Editor对象
    第六步,通过DiskLruCache.Edito写入数据
    第七步,返回数据
    PS:关于key()方法在remove里面详解

    上面使用到了remove方法,莫非就是"删"的操作,那咱们来看下

    (2) ”删“操作——remove()方法
      void remove(Request request) throws IOException {
        cache.remove(key(request.url()));
      }
      public static String key(HttpUrl url) {
        return ByteString.encodeUtf8(url.toString()).md5().hex();
      }
    

    果然remove就是传说中的"删除"操作,
    key()这个方法原来就说获取url的MD5和hex生成的key

    (3) ”改“操作——update()方法
    void update(Response cached, Response network) {
        //用response构造一个Entry对象
        Entry entry = new Entry(network);
        //从命中缓存中获取到的DiskLruCache.Snapshot
        DiskLruCache.Snapshot snapshot = ((CacheResponseBody) cached.body()).snapshot;
        //从DiskLruCache.Snapshot获取DiskLruCache.Editor()对象
        DiskLruCache.Editor editor = null;
        try {
          editor = snapshot.edit(); // Returns null if snapshot is not current.
          if (editor != null) {
            //将entry写入editor中
            entry.writeTo(editor);
            editor.commit();
          }
        } catch (IOException e) {
          abortQuietly(editor);
        }
      }
    

    根据上述代码大体流程如下:
    第一步,首先要获取entry对象
    第二步,获取DiskLruCache.Editor对象
    第三步,写入entry对象

    (4) ”查“操作——get()方法
     Response get(Request request) {
        //获取url经过MD5和HEX的key
        String key = key(request.url());
        DiskLruCache.Snapshot snapshot;
        Entry entry;
        try {
         //根据key来获取一个snapshot,由此可知我们的key-value里面的value对应的是snapshot
          snapshot = cache.get(key);
          if (snapshot == null) {
            return null;
          }
        } catch (IOException e) {
          // Give up because the cache cannot be read.
          return null;
        }
        //利用前面的Snapshot创建一个Entry对象。存储的内容是响应的Http数据包Header部分的数据。snapshot.getSource得到的是一个Source对象 (source是okio里面的一个接口)
        try {
          entry = new Entry(snapshot.getSource(ENTRY_METADATA));
        } catch (IOException e) {
          Util.closeQuietly(snapshot);
          return null;
        }
    
        //利用entry和snapshot得到Response对象,该方法内部会利用前面的Entry和Snapshot得到响应的Http数据包Body(body的获取方式通过snapshot.getSource(ENTRY_BODY)得到)创建一个CacheResponseBody对象;再利用该CacheResponseBody对象和第三步得到的Entry对象构建一个Response的对象,这样该对象就包含了一个网络响应的全部数据了。
        Response response = entry.response(snapshot);
        //对request和Response进行比配检查,成功则返回该Response。匹配方法就是url.equals(request.url().toString()) && requestMethod.equals(request.method()) && OkHeaders.varyMatches(response, varyHeaders, request);其中Entry.url和Entry.requestMethod两个值在构建的时候就被初始化好了,初始化值从命中的缓存中获取。因此该匹配方法就是将缓存的请求url和请求方法跟新的客户请求进行对比。最后OkHeaders.varyMatches(response, varyHeaders, request)是检查命中的缓存Http报头跟新的客户请求的Http报头中的键值对是否一样。如果全部结果为真,则返回命中的Response。
        if (!entry.matches(request, response)) {
          Util.closeQuietly(response.body());
          return null;
        }
    
        return response;
      }
    
    

    总结上面流程大体是:
    第一步 获取key
    第一步 获取DiskLruCache.Snapshot对象
    第三步 获取Entry对象
    第四步 获取response
    第五步 检查是response

    通过对上述增删改查的分析,我们可以得出如下结论

    方法 返回值
    DiskLruCache.get(String) 可以获取DiskLruCache.Snapshot
    DiskLruCache.remove(String) 可以移除请求
    DiskLruCache.edit(String) 可以获得一个DiskLruCache.Editor对象
    DiskLruCache.Editor.newSink(int) 可以获得一个sink流
    DiskLruCache.Snapshot.getSource(int) 可以获取一个Source对象。
    DiskLruCache.Snapshot.edit() 可以获得一个DiskLruCache.Editor对象

    DiskLruCache是OKHTTP的缓存的精髓,由于篇幅限制,在下一章讲解

    相关文章

      网友评论

      • 天马呵呵拳:还是有点不明白,返回给上一个拦截器的response中的cacheResponse和networkResponse的body一定要设为null啊
      • bb6128566ef8:请教一下,客户端缓存这块,从对比缓存贴图来看,检查缓存是否过期,还是要请求一次服务器的:1.不过期,服务器通知客户端不过期,取缓存;2.过期,服务器返回最新数据和缓存规则。但从代码来看,都是调用下层的拦截器获取到一个response,难道这两种情况得到的response有所不同?服务器是不知道客户端是否做了缓存的吧?那第1种情况服务器也还是要返回数据的啊?如果第1种情况返回了数据,那客户端的缓存好像没什么意义了啊?
        dreamruner:CacheInterceptor的intercept流程再仔细看一下,它会根据缓存策略和本地是否有缓存来综合判断的,不会直接去请求网络,只有调用了proceed方法才是走到下一个拦截器并最终发起网络请求的.
      • 111123123:响应头部 意义
        Cache-Control:public 响应被公有缓存,移动端无用

        重复了
        隔壁老李头:@裸奔的牛 是的,谢谢
      • 7jiangxu:强制缓存和对比缓存 贴图重复了,都是一样的
        隔壁老李头:已修改
        隔壁老李头:是的,谢谢!我晚上回家改一下

      本文标题:OKHttp源码解析(六)--中阶之缓存基础

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