漫聊开源源码——Okhttp3 (上篇), 缓存那点儿零碎

前言

Ok3 源码学习是我去年给自己立的flag,同时也是我去年难得兑现的几个flag之一,这里我想再重温下之前的学习状态,整理下我当时学习的思路。OK3的源码非常多,如果算上Okio,那就更多了,而且还涉及到很多我了解很少的网络知识(比如这篇要讲的OK3 缓存,前半部分我几乎先把http 的缓存套路说明白),庆幸的是OK3 的源码注释写的非常详尽,代码结构设计的也不错,理解起来不难,学起来收获满满。这个系列我准备分三篇博客来讲,今天先聊缓存。

一、缓存概述

1.1、什么是缓存?

这里先给大家看一张图,如果大家学过计算机组成原理这门课的话,想必一定眼熟:
在这里插入图片描述
简单描述下,Cache高速缓冲存储器,其作用是为了更好的利用局部性原理(这个可以不用理解),减少CPU访问主存的次数(重点)。简单地说,CPU正在访问的指令和数据,其可能会被以后多次访问到,或者是该指令和数据附近的内存区域,也可能会被多次访问。因此,第一次访问这一块区域时,将其复制到cache中,以后访问该区域的指令或者数据时,就不用再从主存中取出。
总结一句话:

缓存:方便用户快速获取数据的一种存储方式

1.2、缓存的特点?

  • 1、缓存载体与持久载体总是相对的,体量远远小于持久载体,成本高于持久载体,速度高于持久载体。
  • 2、缓存需要页面置换算法,将旧页面去掉换成新页面,如最久未使用算法(LRU)、先进先出算法(FIFO)、最紧最小使用算法(LFU)、非最紧使用算法(NMRU)。
  • 3、可溯源,如果没有命中缓存,就需要从原始地址获取,这个步骤叫做”回源头",CDN厂商会标注"回源率"。

1.3、移动端网络使用缓存的意义

  • 1、减少请求次数,减轻服务器压力。
  • 2、本地数据读取更快,让页面不会空白几百毫秒。
  • 3、在无网络的情况下提供数据。
    总之,核心就是:

提升用户体验

二、HTTP缓存机制

OKhttp 3的缓存策略原理,本质上就是对HTTP缓存机制的代码实现,因此只要我们把HTTP缓存机制理解了,那么后面在去看OKhttp 3的缓存代码会非常非常的容易,比如在第三部分我会重点分析的CacheControlCacheInterceptor以及CacheStrategy

2.1、缓存的分类

1、按照**“端”**分类:

  • 服务器缓存(也称网关缓存),广泛使用的CDN也是一种服务端缓存,目的都是让用户的请求走"捷径",并且都是缓存图片、文件等静态资源。
  • 客户端缓存,一般是浏览器缓存,目的就是加速各种静态资源的访问,提升用户体验。

2、按照“是否向服务器发起请求进行对比”分类:

  • 强制缓存
  • 对比缓存
    OkHttp 3的缓存策略实现就是从按照“是否向服务器发起请求进行对比”分类进行实现的。因此接下来我们将重点介绍“强制缓存”、“对比缓存”的实现原理。

2.2、强制缓存

2.2.1、实现原理:当客户端第一次请求数据时,服务端返回了缓存的过期时间(Expires与Cache-Control)。在第二次请求数据时,如果已存在缓存数据,且缓存有效命中,则使用缓存数据库中的数据,无需请求网络。如果缓存失效或者没有命中,则请求网络,并将网络数据和缓存规则一并存入缓存数据库中。
文字看起来有点绕,那么用图例来说明下:
在这里插入图片描述
我将请求的三种情况用了三种颜色做了标注。这里我说下关于缓存失效的问题,在服务端返回的响应头信息中,有个max-age 字段,对应的值就是最大响应时间,小于这个时间内,缓存是有效的。超过这个时间,缓存是无效的。
2.2.2 设置缓存。关于设置强制缓存,我们需要在请求头中,设置expires/Cache-Control:服务器端返回的到期时间,即下一次请求时,请求时间小于服务器返回的到期时间。Cache-Control默认是private。这里我分享下常用的指令信息:
在这里插入图片描述
在这里插入图片描述
最后再说下,强制缓存的优先级要比接下来要说的对比缓存优先级高

2.3、对比缓存

2.3.1、实现原理:当客户端第一请求数据时,服务端会将缓存标识(Last-Modified/If-Modified-Since与Etag/If-None-Match)与数据一起返回给客户端,客户端将两种数据。都备份到缓存中,再次请求数据时,客户端将上次备份的缓存标识发送给服务端,服务端根据缓存标识进行判断,如果返回304,则表示通知客户端可以继续使用缓存。文字看起来不明白,直接上图:
在这里插入图片描述
这里我还是将请求的三种情况,使用三种颜色进行标注。在介绍强制缓存的最后,我提到了强制缓存的优先级是最高的。如果强制缓存的判断是有效的,则直接读取缓存即可。而对比缓存的存在就是在强制缓存失效的情况下做的二次判断。如果我们配置了对比缓存的策略,且服务端校验当前缓存标识是有效的,即使在强制缓存阶段判断不能使用使用缓存,只要服务端确定当前内容资源服务端没有修改,那么还是可以继续使用缓存的,因此看图也发现,对比缓存相比强制缓存,又多了一次服务端校验缓存的步骤 ,这样做的好处就是可以节约流量,避免资源浪费。
2.3.2 设置缓存
设置对比缓存有两种方式。

  • 1、通过设置Last-Modified/If-Modified-Since。Last-Modified:资源最后的修改日期。在Response Header中,服务器在响应请求时,告诉客户端资源最后的修改时间。If-Modified-Since:比较资源的更新时间。在Request Header中,当客户端再次请求时,通知服务器上次请求时返回的资源最后修改的时间。

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

  • 2、ETag/If-None-Match(优先级高于Last-Modified/If-Modified-Since )。ETag:资源的匹配信息。在Response Header中,服务器在响应请求时,告诉客户端当前资源在服务器的唯一标识(生成规则由服务器决定)。If-None-Match:比较实体标记符。在Request Header中,当客户端再次请求时,通过此字段通知服务器客户端缓存数据的唯一标识.

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

最后,我将强制缓存和对比缓存结合在一起用一张图来理解Http缓存执行流程。
在这里插入图片描述

三、OKHttp 3缓存机制与实现

OKHttp 3缓存机制用的就是Http 缓存的套路。因此,这一节我们重点聊聊实现。来,先奉上一张OkHttp 3 总原理图。
在这里插入图片描述
总的原理图我这次先不分析,下篇我会单独阐述这图的原理。放上这张图我是想让大家明白,接下来我们要讲的缓存策略实现,就是上图中六大拦截器中的CacheInterceptor,以及它的好兄弟们(CacheControlCacheStrategyCache以及DiskLruCache)之间的故事,看这几个类的类名,发现他们都和Cache有关,受至于篇幅原因,这里我仅部分类重点讲。

3.1、CacheControl

这个类是对HTTP的Cache-Control头部的描述,内部通过一个Build进行设置值,获取值可以通过CacheControl对象进行获取。在前面讲强制、对比缓存的设置时,提到了设置请求头关键字。而这个类的作用就是对那些关键字就行描述。这里我再补充些:

  • 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的响应。
    这个类最核心的方法就是parse,重点看下这个:
 // (返回缓存指令,缓存控制。主要用在解析请求头、响应头,最后交给缓存策略类CacheStrategy来处理,最 后在CacheInterceptor调用缓存策略来判断是否要写缓存)
  public static CacheControl parse(Headers headers) {
    
    
	......
	// 上面的不是重点,省略
	for (int i = 0, size = headers.size(); i < size; i++) {
    
    
      String name = headers.name(i);
      String value = headers.value(i);
	......
	    // 根据对请求头信息的解析,判断此次请求是否设置了缓存校验
		if ("no-cache".equalsIgnoreCase(directive)) {
    
    
          noCache = true;
        } else if ("no-store".equalsIgnoreCase(directive)) {
    
    
          noStore = true;
        } else if ("max-age".equalsIgnoreCase(directive)) {
    
    
          maxAgeSeconds = HttpHeaders.parseSeconds(parameter, -1);
        } else if ("s-maxage".equalsIgnoreCase(directive)) {
    
    
          sMaxAgeSeconds = HttpHeaders.parseSeconds(parameter, -1);
        } else if ("private".equalsIgnoreCase(directive)) {
    
    
          isPrivate = true;
        } else if ("public".equalsIgnoreCase(directive)) {
    
    
          isPublic = true;
        } else if ("must-revalidate".equalsIgnoreCase(directive)) {
    
    
          mustRevalidate = true;
        } else if ("max-stale".equalsIgnoreCase(directive)) {
    
    
          maxStaleSeconds = HttpHeaders.parseSeconds(parameter, Integer.MAX_VALUE);
        } else if ("min-fresh".equalsIgnoreCase(directive)) {
    
    
          minFreshSeconds = HttpHeaders.parseSeconds(parameter, -1);
        } else if ("only-if-cached".equalsIgnoreCase(directive)) {
    
    
          onlyIfCached = true;
        } else if ("no-transform".equalsIgnoreCase(directive)) {
    
    
          noTransform = true;
        } else if ("immutable".equalsIgnoreCase(directive)) {
    
    
          immutable = true;
        }
	}
  }

关于请求、响应头信息是设置和读取,在BridgeInterceptor类中实现的。

3.2、CacheStrategy

这个类封装了缓存策略,里面实现的策略本质都是RFC标准文档里面写死的。同时利用networkRequest、cacheResponse这两个参数生成最终的策略,将networkRequest与cacheResponse这两个值输入,处理之后再将这两个值输出。这个类重点看下下面的这个方法:

/** Returns a strategy to use assuming the request can use the network. */
    private CacheStrategy getCandidate() {
    
    
      // No cached response.
      // 1. 如果缓存没有命中,就直接进行网络请求。
      if (cacheResponse == null) {
    
    
        return new CacheStrategy(request, null);
      }

      // Drop the cached response if it's missing a required handshake.
      // 2. 如果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.
      // 3. 根据response状态码,Expired时间和是否有no-cache标签就行判断是否进行直接访问
      if (!isCacheable(cacheResponse, request)) {
    
    
        return new CacheStrategy(request, null);
      }

      // 4. 如果请求header里有"no-cache"或者右条件GET请求(header里带有ETag/Since标签),则直接连接。
      CacheControl requestCaching = request.cacheControl();
      if (requestCaching.noCache() || hasConditions(request)) {
    
    
        return new CacheStrategy(request, null);
      }

      // immutable : 不可变的,表示当前的缓存有效
      CacheControl responseCaching = cacheResponse.cacheControl();
      if (responseCaching.immutable()) {
    
    
        return new CacheStrategy(null, cacheResponse);
      }

      // 计算当前Age的时间戳:now - sent + age
      long ageMillis = cacheResponseAge();
      // 刷新时间,一般服务器设置为max-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());
      }

      // 最大验证时间,如果响应(服务器)那边不是必须验证并且存在最大验证秒数,则更新最大验证时间
      long maxStaleMillis = 0;
      if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
    
    
        maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
      }

      // 5.持续时间+最短刷新时间<上次刷新时间+最大验证时间 则直接返回上次缓存,即:缓存在过期时间内则可以直接使用
      // 也可以理解就是现在时间(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());
      }

      // Find a condition to add to the request. If the condition is satisfied, the response body
      // will not be transmitted.
      // 6. 如果缓存过期,且有ETag等信息,则发送If-None-Match、If-Modified-Since、If-Modified-Since等条件请求,交给服务端判断处理
      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();
      // 返回有条件的缓存request策略
      return new CacheStrategy(conditionalRequest, cacheResponse);
    }

总结下上面的代码原理:

  • 1、如果networkRequest为null,cacheResponse为null:only-if-cached(表明不进行网络请求,且缓存不存在或者过期,一定会返回503错误)。
  • 2、如果networkRequest为null,cacheResponse为non-null:不进行网络请求,而且缓存可以使用,直接返回缓存,不用请求网络。
  • 3、如果networkRequest为non-null,cacheResponse为null:需要进行网络请求,而且缓存不存在或者过期,直接访问网络。
  • 4、如果networkRequest为non-null,cacheResponse为non-null:Header中含有ETag/Last-Modified标签,需要在条件请求下使用,还是需要访问网络。

缓存策略的最终的使用,是在CacheInterceptor类中。在CacheStrategy中提供了一个方法可以获取当前的策略:

/**
     * 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;
    }

3.3、CacheInterceptor

负责读取缓存以及更新缓存.
先放上一张这个类的执行的流程图
在这里插入图片描述
接下来分析下代码的实现,拦截器的核心方法就是intercept。

 @Override public Response intercept(Chain chain) throws IOException {
    
    
    // 1. 读取候选缓存
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();

     // 2. 创建缓存策略,获取策略中的请求、响应
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;

    if (cache != null) {
    
    
      // 跟踪满足缓存策略CacheStrategy的响应
      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(Util.EMPTY_RESPONSE)
          .sentRequestAtMillis(-1L)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build();
    }

    // If we don't need the network, we're done.
    // 4. 根据策略,不使用网络,有缓存的直接返回
    if (networkRequest == null) {
    
    
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }

    // 5. 前面两个都没有返回,继续执行下一个Interceptor,即ConnectInterceptor。
    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.
    // 6. 接收到网络结果,如果响应code式304,则使用缓存,返回缓存结果。
    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()).
        // 跟踪一个满足缓存条件的GET请求 ,并更新缓存
        cache.trackConditionalCacheHit();
        cache.update(cacheResponse, response);
        return response;
      } else {
    
    
        closeQuietly(cacheResponse.body());
      }
    }

    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();
    // 读取网络结果,这里有两种情况
    // 1、根据服务端响应的结果,允许使用缓存(!response.cacheControl().noStore() && !request.cacheControl().noStore() )
    // 2、当前的网络请求适合使用缓存,即仅Get请求才会缓存,其他请求就不缓存
    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;
  }

关于OK3的缓存实现就分析到这里。至于缓存的写入和读取,主要是在Cache、DiskLruCache类中。这些操作这里我就不分析了,DiskLruCache很多开源项目中都会使用,只要涉及到有缓存功能的,比如一些开源的图片框架,后面我会单独聊下这个类的实现原理。

总结

OK3的缓存原理相比于它的其它重要模块,理解起来要简单的多,关于缓存原理,重点就是理解Http的缓存原理实现,以及上面说的那三个类的作用。

参考:
OKHttp源码解析(七)–中阶之缓存机制

猜你喜欢

转载自blog.csdn.net/qq_26439323/article/details/107448764