OKHttp3-- HTTP缓存机制解析 缓存处理类Cache和缓存策略类CacheStrategy源码分析 【九】

2019年12月05日 阅读数:21
这篇文章主要向大家介绍OKHttp3-- HTTP缓存机制解析 缓存处理类Cache和缓存策略类CacheStrategy源码分析 【九】,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

系列

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

前言

继续上篇关于缓存拦截器的文章来说述OKHttp内部缓存的实现逻辑,CacheInterceptor内部所涉及的缓存操做和策略主要由如下两个类完成java

  • Cache:OKHttp中缓存具体的操做类web

  • CacheStrategy:缓存策略类,由它决定是使用缓存仍是使用网络请求算法

HTTP缓存机制

HTTP协议详细解释可参考Hypertext Transfer Protocol – HTTP/1.1浏览器

HTTP头部字段定义可参考Header Field Definitions缓存

OKHttp的缓存机制遵循了HTTP协议的缓存机制,因此在了解它以前,咱们先来看下HTTP协议的缓存机制服务器

HTTP的缓存机制一般有两种:客户端缓存和服务端缓存,咱们今天主要介绍下客户端缓存网络

服务端缓存

服务端缓存一般分为代理服务器缓存和反向代理服务器缓存(好比 Nginx反向代理、Squid等)app

客户端缓存

客户端缓存通常指的是浏览器缓存,而它的实现通常有HTTP协议定义的缓存机制(由头部信息决定),非HTTP协议的缓存机制(HTML Meta标签实现)异步

非HTTP协议的缓存机制实现就是在< head>节点中加入< meta>标签,以下

<META HTTP-EQUIV="Pragma" CONTENT="no-cache">

加这句代码就是告诉浏览器当前页面不须要缓存,每次访问都去服务器拉取最新数据;可是它的做用颇有限,仅有IE能识别这段meta标签含义,其它主流浏览器仅识别“Cache-Control: no-store”的meta标签

咱们这里主要讲HTTP协议定义的缓存机制:

这里一般从两个维度来规定浏览器是直接使用缓存中的数据,仍是去服务器获取新的数据:

  • 新鲜度:这对应着缓存中的过时机制,也就是代表缓存的有效期;一个缓存资源须要知足下面的条件之一,浏览器会认为该资源是可使用的,无需向服务器从新拉取

    • 含有完整的过时时间控制头信息(HTTP协议报头),而且仍在有效期内;
    • 浏览器已经使用过这个缓存资源,而且在一个会话中已经检查过新鲜度
  • 校验值:这对应着验证机制;服务器返回资源的时候有时会在头信息带上这个资源的实体标签Etag(Entity Tag),它能够用来做为浏览器再次请求该资源的校验标识。如发现校验标识不匹配,说明资源在服务器已经被修改或过时,浏览器需从新获取资源

接下来咱们从实际场景来看看:

当咱们第一次经过浏览器访问服务器时,确定是没有缓存的,直接向服务器请求数据;当获取到响应后根据头部信息决定是否将其保存在浏览器中,如图
在这里插入图片描述

当再次请求服务器时,再根据头部信息中的字段,好比Expires,Cache-control,Last-Modified , If-Modified-Since ,Etag,If-None-Match等字段来决定是使用缓存仍是从新获取资源;流程如图
在这里插入图片描述

当咱们取出缓存时,先要判断缓存是否过时,那怎么判断呢?有两个头部信息能够知道:

  • 第一个就是Expires:该字段是存在于服务器返回的响应头中,目的是告诉浏览器该资源的过时时间;也就是说当浏览器再次请求的时候若是当前时间早于这个过时时间,那么就不须要请求了,直接使用缓存;若是晚于这个时间,那么再向浏览器请求数据;该字段存在于HTTP/1.0中,格式以下

    Expires: Thu, 12 Mar 2019 12:08:54 GMT
    

    这种机制有一个很是大的问题,由于该字段是存在于响应头中,也就是说它的时间是服务器上的时间,可是客户端的时间是颇有可能与服务器上的时间存在偏差的,好比不在一个时区,用户修改了本身电脑时间等因素,这样这个字段就没有意义了;在HTTP 1.1开始,使用Cache-Control: max-age=秒 替代;并且如今浏览器均默认使用HTTP 1.1,因此它的做用基本忽略

  • 第二个就是Cache-control:它是当前浏览器缓存中很是重要的一个字段,做用与Expires差很少,存在于响应头,都是标注当前资源的有效期;可是它有不少的值,能够指定较为复杂的缓存规则,若是与Expires同时存在,Cache-control的优先级高,它的通常格式以下

    Cache-Control: private, max-age=0, no-cache
    

    能够组合的值有:

    • public:代表该资源或者说响应能够被任何用户缓存,好比客户端,代理服务器等均可以缓存资源,写法:Cache-Control:public
    • private:代表该资源只能被单个用户缓存,默认是private,即只能被客户端缓存,不能被代理服务器缓存,写法:Cache-Control:private
    • max-age:代表该资源的有效时间,单位是s,写法: Cache-Control:max-age=3600,即在获取该资源后3600s内不须要再向服务器获取
    • no-cache:代表客户端须要忽略已存在的缓存,强制每次请求直接发送给服务器,拉取资源,写法:Cache-Control:no-cache
    • no-store:代表该资源不能被缓存,若是缓存了须要删除,写法:Cache-Control:no-store
    • s-maxage:和max-age含义相似,只不过用于public 修饰的缓存,写法:Cache-Control:s-maxage=3600
    • must-revalidate:代表在使用缓存前必需要验证旧资源状态,而且不可以使用过时资源, 写法:Cache-Control:must-revalidate
    • max-stale:代表缓存的资源在过时了但未超过max-stale指定的时间,那么就能够继续使用该缓存,超事后就必须去服务器获取;写法:Cache-Control:max-stale(表明着资源永不过时); Cache-Control:max-stale=3600(代表在缓存过时后的3600秒内还能够继续用)
    • min-fresh:字面意思是最小新鲜度,跟max-age相对应(最大新鲜度),好比max-age=3600,min-fresh=600,那么 他两的差值就是3000,也就是说缓存真正有效时间只有3000s,超过这个时间就要去服务器拉取了
    • only-if-cached:无论缓存是否过时,或者服务端有更新,只要存在缓存就是用它,写法:Cache-Control:only-if-cached
    • no-transform:不得对资源进行转换,即代理服务器不能修改Content-Encoding, Content-Range, Content-Type等HTTP头;由于有时候代理服务器为了节省缓存空间或者提升传输效率,会对图片等进行压缩;写法: Cache-Control:no-transform、
    • immutable:表示资源在有效期内服务器不会对其更改,这样客户端就不须要再发送验证请求头,好比If-None-Match或If-Modified-Since来检测更新,即便用户主动刷新页面,写法:Cache-Control:immutable

上面所列举的Expires和Cache-control是重新鲜度的角度来强制决定客户端缓存,下面所说的是从校验值的角度来与服务器协商验证是使用缓存仍是从新获取数据,也就是上图在判断缓存过时后的流程

这里有几个字段来标识缓存规则:Last-Modified / If-Modified-Since ,Etag / If-None-Match(优先级大于Last-Modified / If-Modified-Since))

  • Etag:服务器在响应客户端请求时,会在响应头带上该字段;它表示该资源在服务器中的惟一标识,生成规则由服务器决定,在Apache中,ETag的值默认是对文件的索引节(INode),大小(Size)和最后修改时间(MTime)进行Hash后获得的
  • If-None-Match:这是在请求头中的字段,值就是Etag的值

它们两的使用逻辑就是:当客户端判断资源过时时(一般使用Cache-Control标识的max-age),若是发现缓存的响应有Etag头部声明,那再次向服务器请求时带上If-None-Match头部,值就是Etag的值,web服务器收到请求后发现有If-None-Match头,就将其与存在服务端的Etag值进行比较;若是匹配,说明该资源没有修改,那就返回304,告诉客户端能够继续使用缓存;若是不匹配,说明资源修改过,那就返回200,从新响应该资源给客户端

接下来就是客户端发现缓存的响应没有Etag声明,那就从Last-Modified / If-Modified-Since进行判断

  • Last-Modified:标识资源在服务器上的最后修改时间,随着响应头带给客户端
  • If-Modified-Since:这是在请求头中的字段,值就是Last-Modified的值

它们两的使用逻辑是:当客户端判断资源过时时,同时缓存的响应头没有Etag声明,若是发现头部有Last-Modified声明,则再次向服务器请求资源时,在请求头带上 If-Modified-Since头部,值就是Last-Modified的值;服务器收到请求后发现有头If-Modified-Since 则与被请求资源的最后修改时间进行比对。若最后修改时间较新,说明资源被改动过,则响应整片资源给客户端,响应码是 200;若最后修改时间较旧,说明资源无修改,则响应304 ,告知浏览器继续使用缓存

通常状况下,使用Cache-Control/Expires会配合Last-Modified/ETag一块儿使用,由于即便服务器设置缓存时间,当用户点击“刷新”按钮时,浏览器会忽略缓存继续向服务器发送请求,这时Last-Modified/ETag就可以起做用,服务器若是资源没有修改就返回304,从而减小响应开销

Etag与Last-Modified

不知道你有没有疑惑,Etag也是判断资源有没有修改,Last-Modified也是判断资源有没有修改,那两个重复功能的存在是否是多余呢?

要知道Last-Modified是出如今HTTP1.0中的,可是它有几个问题:

  • Last-Modified表示的最后修改时间只能精确到秒级,若是某些文件在1秒钟之内,被修改屡次的话,它就不能准确标注文件的新鲜度
  • 一些文件也许会周期性的更改,可是他的内容并不改变(仅仅改变的修改时间),这个时候咱们并不但愿客户端认为这个文件被修改了,而从新GET
  • 服务器可能没有获取准确的修改时间,或者与代理服务器时间不一致

在HTTP1.1出现的Etag就主要就是为了解决它的几个问题,Etag是服务器自动生成或者由开发者生成的对应资源在服务器端的惟一标识符,可以更加准确的控制缓存。Last-Modified与ETag是能够一块儿使用的,服务器会优先验证ETag,一致的状况下,才会继续比对Last-Modified,最后才决定是否返回304,关于Etag更详细的能够参考Etag互动百科

用户行为

浏览器的缓存不光与HTTP协议有关,还与用户的行为有关,好比用户手动点击浏览器的刷新按钮,或者按Ctrl+F5进行强制刷新,这些操做都会对从缓存中获取到的响应的头部字段Expires/Cache-Control,Last-Modified/Etag产生影响

用户操做 Expires/Cache-Control Last-Modified/Etag
地址栏回车 有效 有效
页面连接跳转 有效 有效
新开窗口 有效 有效
前进、后退 有效 有效
F5/按钮刷新 无效(浏览器重置max-age=0) 有效
Ctrl+F5强制刷新 无效(重置为no-cache) 无效(请求头丢弃该选项)

Cache

OKHttp提供了缓存机制以将咱们的的HTTP和HTTPS请求的响应缓存到文件系统中,可是它默认是不使用缓存的,因此若是咱们须要使用缓存(强烈推荐使用),就得在实例化OKHttpClient的时候进行相关的配置,以下:

全局缓存配置

OkHttpClient client = new OkHttpClient
                .Builder()
                .cache(new Cache(new File("cache"),1024*1024*10))//接收两个参数,1:私有缓存目录,2:缓存空间大小
                .build();

这里的Cache类是OKHttp内部提供的一个用于缓存实际操做的类,它内部维护了一个匿名内部类实现了InternalCache接口,该接口定义的功能由Cache实现

固然了你也能够本身实现InternalCache接口,采用本身的缓存逻辑,那就经过Build的以下方法进行设置

    void setInternalCache(InternalCache internalCache) {
      this.internalCache = internalCache;
      this.cache = null;
    }

单个请求缓存配置

上面的缓存配置是全局的,也能够对单个请求配置不一样的缓存策略

new Request
         .Builder()
         .cacheControl(new CacheControl())
         .build();

这个CacheControl的构造方法有不少参数,参考上面的HTTP缓存机制那一节;同时CacheControl内部也提供了两个策略供开发者使用

//强制使用网络,不使用缓存
public static final CacheControl FORCE_NETWORK = new Builder().noCache().build();
//强制使用缓存,不使用网络
public static final CacheControl FORCE_CACHE = new Builder()
      .onlyIfCached()
      .maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS)
      .build();
  • 某些状况下,好比用户点击刷新按钮,那么就须要跳过缓存,从新去服务器获取最小数据,那就须要强制使用网络,不使用缓存

    Request request = new Request.Builder()
            .cacheControl(new CacheControl.Builder().noCache().build())
            //.cacheControl(CacheControl.FORCE_NETWORK ) 两种写法同样
            .url("http://publicobject.com/helloworld.txt")
            .build();
    
  • 若是必需要服务器验证下缓存的响应,可使用max-age = 0 指令

        Request request = new Request.Builder()
            .cacheControl(new CacheControl.Builder().maxAge(0, TimeUnit.SECONDS).build())
            .url("http://publicobject.com/helloworld.txt")
            .build();
    
  • 若是某些资源不须要重复向服务器请求,能够设置强制使用缓存

    Request request = new Request.Builder()
              .cacheControl(new CacheControl.Builder().onlyIfCached().maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS).build())
              .url("http://publicobject.com/helloworld.txt")
              .build();
    

给单个请求进行配置最终会添加相应的请求头

   /**
   * 该请求头将会替换掉任何缓存控制标头
   */
   public Builder cacheControl(CacheControl cacheControl) {
      String value = cacheControl.toString();
      if (value.isEmpty()) return removeHeader("Cache-Control");
      return header("Cache-Control", value);
    }

构造方法

接下来看下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);
  }

公开构造方法接收两个参数,而后调用内部重载的构造方法;在下面的这个构造方法能够看到实例化了一个DiskLruCache对象

DiskLruCache这个东西你们应该很熟悉了吧,咱们一般作三级缓存的时候,其中的文件缓存或者说磁盘缓存就是经过它来实现的,使用的是著名的 最近最少使用算法(Least recently used),因此能够明白OKHttp的缓存实际上是经过DiskLruCache实现的,至于这个类的实现原理,你们能够在网上查一查,资料不少


Cache.put

先看下这个Cache类是如何缓存咱们获取到的响应

  CacheRequest put(Response response) {
    // 获取网络请求方法
    String requestMethod = response.request().method();
    // 验证请求方法的合法性,具体什么方法不能缓存见下方
    if (HttpMethod.invalidatesCache(response.request().method())) {
      try {
        // 若是是这些请求方法就移除缓存
        remove(response.request());
      } catch (IOException ignored) {
        // The cache cannot be written.
      }
      return null;
    }
    // 若是请求方法不是get请求,那就直接返回null
    if (!requestMethod.equals("GET")) {
      // 不作非get请求的缓存,虽然其它方法的响应能够缓存,可是作起来成本太大且效率低下,因此放弃
      return null;
    }
    // 若是响应头含有 * 字符,那也不缓存,直接返回null
    if (HttpHeaders.hasVaryAll(response)) {
      return null;
    }
    
    // 建立Entry对象,这个对象封装了响应的一些信息,见下方
    Entry entry = new Entry(response);
    // 建立编辑对象 这个操做相似于SharedPerference
    DiskLruCache.Editor editor = null;
    try {	
      // 经过DiskLruCache建立editor对象(须要将url转换成key,方法见下方)
      editor = cache.edit(key(response.request().url()));
      if (editor == null) {
        return null;
      }
      // 将entry封装的部分信息写入缓存,不包括响应体,该方法见下方
      entry.writeTo(editor);
      // 返回CacheRequestImpl对象给拦截器,用来缓存响应体
      return new CacheRequestImpl(editor);
    } catch (IOException e) {
      abortQuietly(editor);
      return null;
    }
  }

HttpMethod.invalidatesCache

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

若是是以上这些请求方法,那么获取到的响应将不会进行缓存

HTTP请求方法一览:

  • OPTIONS:获取服务器支持的HTTP请求方法和用来检查服务器的性能
  • GET:请求获取Request-URI所标识的资源
  • POST:在Request-URI所标识的资源后附加新的数据,一般咱们用来提交表单
  • PUT:请求服务器存储一个资源,并用Request-URI做为其标识
  • PATCH:是对PUT方法的补充,用来对已知资源进行局部更新
  • DELETE:请求服务器删除Request-URI所标识的资源
  • HEAD:请求获取由Request-URI所标识的资源的响应消息报头
  • MOVE:请求服务器将指定的页面移至另外一个网络地址

Cache.Entry

  private static final class Entry {
    /** Synthetic response header: the local time when the request was sent. */
    private static final String SENT_MILLIS = Platform.get().getPrefix() + "-Sent-Millis";

    /** Synthetic response header: the local time when the response was received. */
    private static final String RECEIVED_MILLIS = Platform.get().getPrefix() + "-Received-Millis";

    private final String url;
    private final Headers varyHeaders;
    private final String requestMethod;
    private final Protocol protocol;
    private final int code;
    private final String message;
    private final Headers responseHeaders;
    private final Handshake handshake;
    private final long sentRequestMillis;
    private final long receivedResponseMillis;

    ...... 

}

Entry是Cache的一个静态内部类,封装了请求的一些信息,好比请求url,请求头,请求方法,请求协议类型;响应码,响应信息,响应头,TLS握手信息,请求发送时间,响应获取时间

Cache.key

 public static String key(HttpUrl url) {
    return ByteString.encodeUtf8(url.toString()).md5().hex();
  }

将url生成key

  • 获取url的utf-8格式不可变字节序列
  • 进行md5加密
  • 获取16进制形式字符串

Cache.Entry.writeTo

    public void writeTo(DiskLruCache.Editor editor) throws IOException {
      BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA));

      sink.writeUtf8(url)
          .writeByte('\n');
      sink.writeUtf8(requestMethod)
          .writeByte('\n');
      sink.writeDecimalLong(varyHeaders.size())
          .writeByte('\n');
      for (int i = 0, size = varyHeaders.size(); i < size; i++) {
        sink.writeUtf8(varyHeaders.name(i))
            .writeUtf8(": ")
            .writeUtf8(varyHeaders.value(i))
            .writeByte('\n');
      }

      sink.writeUtf8(new StatusLine(protocol, code, message).toString())
          .writeByte('\n');
      sink.writeDecimalLong(responseHeaders.size() + 2)
          .writeByte('\n');
      for (int i = 0, size = responseHeaders.size(); i < size; i++) {
        sink.writeUtf8(responseHeaders.name(i))
            .writeUtf8(": ")
            .writeUtf8(responseHeaders.value(i))
            .writeByte('\n');
      }
      sink.writeUtf8(SENT_MILLIS)
          .writeUtf8(": ")
          .writeDecimalLong(sentRequestMillis)
          .writeByte('\n');
      sink.writeUtf8(RECEIVED_MILLIS)
          .writeUtf8(": ")
          .writeDecimalLong(receivedResponseMillis)
          .writeByte('\n');

      if (isHttps()) {
        sink.writeByte('\n');
        sink.writeUtf8(handshake.cipherSuite().javaName())
            .writeByte('\n');
        writeCertList(sink, handshake.peerCertificates());
        writeCertList(sink, handshake.localCertificates());
        // 在旧的缓存响应和HttpsURLConnection上tls握手版本是null
        if (handshake.tlsVersion() != null) {
          sink.writeUtf8(handshake.tlsVersion().javaName())
              .writeByte('\n');
        }
      }
      sink.close();
    }

将Entry封装的url,请求方法,请求头;响应行(这里面包括请求协议类型,响应码,响应信息),响应头,发送请求的时间,获取响应的时间,TLS握手信息写入到缓存

总结

经过对put方法的分析咱们能够知道:

  • OkHttp只支持缓存GET方法的响应
  • 若是响应头含有*字符也不缓存
  • Cache类中并不会直接将响应体写入缓存,而是交给构建一个CacheRequestImpl对象交给拦截器去操做

Cache.get

继续看从缓存中获取响应的方法

CacheInterceptor的intercept方法中第一句代码就是调用Cache类的get方法获取缓存的响应,那咱们来看下它的具体实现

  Response get(Request request) {
    // 将请求url转化成可使用的key
    String key = key(request.url());
    // 定义缓存快照对象
    DiskLruCache.Snapshot snapshot;
    Entry entry;
    try {
      // 经过DiskLruCache对象获取一个该key对象的缓存快照
      snapshot = cache.get(key);
      // 若是快照是null,说明没有缓存响应,直接返回null
      if (snapshot == null) {
        return null;
      }
    } catch (IOException e) {
      // 出现异常 不能读取缓存
      return null;
    }

    try {
      // 建立Entry对象,将快照中的缓存信息封装到Entry对象
      entry = new Entry(snapshot.getSource(ENTRY_METADATA));
    } catch (IOException e) {
      Util.closeQuietly(snapshot);
      return null;
    }
	// 将缓存中的数据构建成一个响应 见下方
    Response response = entry.response(snapshot);
    // 经过比对请求和响应的相关字段,来判断是不是改请求对应的响应 见下方
    if (!entry.matches(request, response)) {
      Util.closeQuietly(response.body());
      return null;
    }

    return response;
  }

Cache.Entry.response

    public Response response(DiskLruCache.Snapshot snapshot) {
      String contentType = responseHeaders.get("Content-Type");
      String contentLength = responseHeaders.get("Content-Length");
      Request cacheRequest = new Request.Builder()
          .url(url)
          .method(requestMethod, null)
          .headers(varyHeaders)
          .build();
      return new Response.Builder()
          .request(cacheRequest)
          .protocol(protocol)
          .code(code)
          .message(message)
          .headers(responseHeaders)
          .body(new CacheResponseBody(snapshot, contentType, contentLength))
          .handshake(handshake)
          .sentRequestAtMillis(sentRequestMillis)
          .receivedResponseAtMillis(receivedResponseMillis)
          .build();
    }

经过Entry中的信息构建响应中非响应体的数据,而响应报文body是经过CacheResponseBody构建,CacheResponseBody又是经过缓存快照和内容类型,内容长度构成(见下方),最终就构成了一个完整的响应

Cache.CacheResponseBody

CacheResponseBody也是Cache中的一个静态内部类

  private static class CacheResponseBody extends ResponseBody {
    final DiskLruCache.Snapshot snapshot;
    private final BufferedSource bodySource;
    private final String contentType;
    private final String contentLength;

    public CacheResponseBody(final DiskLruCache.Snapshot snapshot,
        String contentType, String contentLength) {
      this.snapshot = snapshot;
      this.contentType = contentType;
      this.contentLength = contentLength;

      Source source = snapshot.getSource(ENTRY_BODY);
      bodySource = Okio.buffer(new ForwardingSource(source) {
        @Override 
        public void close() throws IOException {
          snapshot.close();
          super.close();
        }
      });
    }
  }

这里将快照中的关于响应体的数据Source 读到BufferedSource中

还记得咱们日常经过Response.body().byteStream() 拿到响应体的数据流,这个流就是BufferedSource的inputStream方法提供的流

Cache.Entry.matches

    public boolean matches(Request request, Response response) {
      return url.equals(request.url().toString())
          && requestMethod.equals(request.method())
          && HttpHeaders.varyMatches(response, varyHeaders, request);
    }

将Entry封装的从缓存读取的响应的数据和传递过来的Request中的数据进行对比,判断是否匹配

总结

获取缓存的方法比较简单,经过url获取缓存快照,若是没有就返回null;反之经过快照构建Entry对象,而后经过Entry和Snapshot 构建完整的响应,最后比对Request和Response,若是匹配就返回


CacheStrategy

缓存拦截器CacheInterceptor决定使用缓存仍是网络请求是由这个策略类决定的,它里面维护两个变量:

  /** 若是最终这个变量为null,那就不能使用网络;反之就经过网络发送请求. */
  public final Request networkRequest;

  /** 若是最终这个变量为null,那就不能使用缓存;反之就返回缓存的响应. */
  public final Response cacheResponse;

在上篇文章咱们分析拦截器的拦截方法开头有这么一段代码,而且分析完后能够看到CacheStrategy 在拦截方法里起到了很是重要的做用

  @Override 
  public Response intercept(Chain chain) throws IOException {

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

接下来看看是怎么获取这个实例的

CacheStrategy.Factory

    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();
        // 遍历header,保存Date、Expires、Last-Modified、ETag、Age等缓存机制相关字段的值
        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);
          }
        }
      }
    }

Factory是CacheStrategy的一个静态内部类,用来获取缓存的响应信息,而后根据这些信息生成缓存策略

最后经过get方法获取CacheStrategy实例

CacheStrategy.Factory.get

    public CacheStrategy get() {
      // 获取CacheStrategy 对象
      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;
    }

经过getCandidate方法获取CacheStrategy 对象,可是下面有个判断,意思就是经过对缓存的解析,获得的结果是咱们须要经过网络请求获取响应;可是咱们的请求头设置了only-if-cached,那这里的意思就不是说只是用缓存,而是不要使用网络;那这两个就出现了矛盾,OKHttp的解决方法是直接new一个新的CacheStrategy返回,参数是null,这样在缓存拦截器中将会构建一个504错误返回给用户

CacheStrategy.Factory.getCandidate

    private CacheStrategy getCandidate() {
      // 若是没有缓存 那就将response置null,直接进行网络请求
      if (cacheResponse == null) {
        return new CacheStrategy(request, null);
      }

      // 若是是https链接,但没有握手信息,那就进行网络请求
      if (request.isHttps() && cacheResponse.handshake() == null) {
        return new CacheStrategy(request, null);
      }

      //该方法在解析缓存拦截器的文章里分析过了,主要是经过响应码以及头部缓存控制字段判断响应能不能缓存
      // 若是不能缓存那就进行网络请求
      if (!isCacheable(cacheResponse, request)) {
        return new CacheStrategy(request, null);
      }

       // 取出请求头缓存控制对象
      CacheControl requestCaching = request.cacheControl();
      // noCache代表要忽略本地缓存
      // If-Modified-Since/If-None-Match说明缓存过时,须要服务端验证
      // 这两种状况就须要进行网络请求
      if (requestCaching.noCache() || hasConditions(request)) {
        return new CacheStrategy(request, null);
      }

      // 该响应已缓存的时长
      long ageMillis = cacheResponseAge();//见下方
      // 该响应能够缓存的时长
      long freshMillis = computeFreshnessLifetime();//见下方

      if (requestCaching.maxAgeSeconds() != -1) {
        // 取出二者最小值
        //走到这里 ,从computeFreshnessLifetime方法能够知道就是拿Request和Response的CacheControl头中
        // max-age值做比较
        freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
      }

      long minFreshMillis = 0;
      if (requestCaching.minFreshSeconds() != -1) {
        // 这里是取出min_fresh值,即缓存过时后还能继续使用的时长
        minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
      }

      long maxStaleMillis = 0;
      //取出响应头缓存控制字段
      CacheControl responseCaching = cacheResponse.cacheControl();
      // 第一个判断:是否要求必须去服务器验证资源状态
      // 第二个判断:获取max-stale值,若是不等于-1,说明缓存过时后还能使用指定的时长
      if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
        // 若是不用去服务器验证状态且max-stale值不等于-1,那就取出来
        maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
      }
 
      // 若是响应头没有要求忽略本地缓存
      // 已缓存时长+最小新鲜度时长 < 最大新鲜度时长 + 过时后继续使用时长
      // 经过不等式转换:最大新鲜度时长减去最小新鲜度时长就是缓存的有效期,再加上过时后继续使用时长,那就是缓存极限有效时长
      //若是已缓存的时长小于极限时长,说明还没到极限,对吧,那就继续使用
      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());
      }

      // 走到这里说明缓存真的过时了
      String conditionName;
      String conditionValue;
      if (etag != null) {//判断缓存的响应头是否设置了Etag
        conditionName = "If-None-Match";
        conditionValue = etag;
      } else if (lastModified != null) {//判断缓存的响应头是否设置了lastModified 
        conditionName = "If-Modified-Since";
        conditionValue = lastModifiedString;
      } else if (servedDate != null) {//判断缓存的响应头是否设置了Date
        conditionName = "If-Modified-Since";
        conditionValue = servedDateString;
      } else {
        //若是都没有设置就使用网络请求
        return new CacheStrategy(request, null);
      }

      //复制一份和当前请求同样的头部
      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);
    }

在上面说到HTTP缓存机制的时候提到过新鲜度的概念,作一个通俗的比喻,一根香蕉从树上摘下来,这是新鲜的,而且它有一个维持新鲜的时间跨度,好比三天后就会烂掉,那它的新鲜度就是从树上摘下来放到家里保存的这三天

这个类比中树就是服务器,香蕉就是响应的资源,摘到家里就是客户端请求资源保存在本地,缓存有效期就是三天

HTTP头部跟新鲜度有关的字段有:Age,Expires,Date,Last-Modified,max-age

同时这里再注明这个类里的几个变量

  • Date servedDate:对应着响应头的“Date”字段,服务器认定的报文建立时间和日期
  • long receivedResponseMillis:收到该响应的时间戳
  • int ageSeconds:对应着响应头的“Age”字段,当代理服务器用本身缓存的实体去响应请求时,用该头部代表该实体从产生到如今通过多长时间了
  • long sentRequestMillis:发起请求的时间戳

CacheStrategy.Factory.cacheResponseAge

    private long cacheResponseAge() {
      long apparentReceivedAge = servedDate != null
          ? Math.max(0, receivedResponseMillis - servedDate.getTime())
          : 0;
      long receivedAge = ageSeconds != -1
          ? Math.max(apparentReceivedAge, SECONDS.toMillis(ageSeconds))
          : apparentReceivedAge;
      long responseDuration = receivedResponseMillis - sentRequestMillis;
      long residentDuration = nowMillis - receivedResponseMillis;
      return receivedAge + responseDuration + residentDuration;
    }

该方法是返回响应的已缓存时间:

  • 计算收到响应的时间与服务器建立该响应时间的差值apparentReceivedAge
  • 取出apparentReceivedAge 与ageSeconds最大值并赋予receivedAge
  • 计算从发起请求到收到响应的时间差responseDuration
  • 计算如今与收到响应的时间差residentDuration
  • 三者加起来就是响应已存在的总时长

CacheStrategy.Factory.computeFreshnessLifetime

    // 获取缓存的新鲜度,或者说能够缓存的时长
    private long computeFreshnessLifetime() {
      // 取出响应头部
      CacheControl responseCaching = cacheResponse.cacheControl();
      //若是设置了max-age,那就返回它的值
      if (responseCaching.maxAgeSeconds() != -1) {
        return SECONDS.toMillis(responseCaching.maxAgeSeconds());
      } else if (expires != null) {//若是设置了过时时间
        long servedMillis = servedDate != null
            ? servedDate.getTime()
            : receivedResponseMillis;
        // 计算过时时间与产生时间的差就是能够缓存的时长
        long delta = expires.getTime() - servedMillis;
        return delta > 0 ? delta : 0;
      } else if (lastModified != null && cacheResponse.request().url().query() == null) {//当上述2个字段都不存在时 进行试探性过时计算
      
        // As recommended by the HTTP RFC and implemented in Firefox, the
        // max age of a document should be defaulted to 10% of the
        // document's age at the time it was served. Default expiration
        // dates aren't used for URIs containing a query.
        long servedMillis = servedDate != null
            ? servedDate.getTime()
            : sentRequestMillis;
        long delta = servedMillis - lastModified.getTime();
        return delta > 0 ? (delta / 10) : 0;
      }
      //走到这里,说明缓存不能继续使用了,须要进行网络请求
      return 0;
    }

试探性过时时间

采用LM_Factor算法计算,方法以下
time_since_modify = max(0,date - last-modified);
freshness_time = (int)(time_since_modify*lm_factor)

详细解释下:
time_since_modify = max(0,date - last-modified);就是将服务器响应时间(date) 减去 服务器最后一次修改资源的时间(last-modified) 获得一个时间差,用这个时间差和0比较取最大值获得time_since_modify,
freshness_time = (int)(time_since_modify*lm_factor);前面咱们已经获得time_since_modify 这里咱们取其中一小段时间做为过时时间,lm_factor就是这一小段的比例,在okhttp中比例是10%

总结

总结一波使用缓存仍是使用网络的策略

  • 若是从Cache获取的Response是null,那就须要使用网络请求获取响应
  • 若是是Https请求,可是又丢失了握手信息,那也不能使用缓存,须要进行网络请求
  • 若是从响应码判断响应不能缓存且响应头有no-store标识,那就须要进行网络请求
  • 若是请求头有no-cache标识或者有If-Modified-Since/If-None-Match,那么须要进行网络请求
  • 若是响应头没有no-cache标识,且缓存时间没有超过极限时间,那么可使用缓存,不须要进行网络请求
  • 若是缓存过时了,可是响应头没有设置Etag,Last-Modified,Date,那就直接使用网络请求
  • 将上一步的头部信息添加到请求头,构建策略类并返回,这一步的效果其实也是须要使用网络请求

一路分析下来发现Request和Resonse都为null的状况只有一种,就是在get方法里面

      if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
        // We're forbidden from using the network and the cache is insufficient.
        return new CacheStrategy(null, null);
      }

也就是说getCandidate方法告诉客户端须要进行网络请求,可是咱们设置的请求头有only-if-cached标识(只要有缓存就使用缓存),那就发送矛盾,因此就二者都为null,返回一个504给用户

抛开这种特殊状况,只有Request为null的状况只有一种,也就是响应头没有no-cache标识,且缓存时间没有超过极限时间,这时候须要使用缓存;只有Response为null的状况就有5种,这都须要进行网络请求;二者都不为nul只有一种状况,这也须要进行网络请求(可能并非从新获取资源,而只是根据头部信息向服务器验证资源的有效性)