HTTP缓存机制在iOS中的应用和体现

1、 什么是缓存

Web 缓存是能够保存文档副本的HTTP设备。算法

HTTP缓存通常为两种,本地缓存和代理缓存。本地缓存就是客户端设备中的缓存,代理缓存就是缓存代理服务器,常见的就 是 CDN。缓存

2、缓存机制

1. 缓存机制

缓存的机制是针对客户端-缓存设备-源站的交互而言的,缓存的处理机制以下: bash

缓存机制

如上图所示,通常而言,缓存是否新鲜采用 Cache-Control/Expires 进行判断,也叫作强制缓存。服务器的再验证通常采用 If-None-Match + ETag 或者 If-Modified-Since + Last-Modified 的“条件get”请求来判断,也叫作对比缓存。服务器

2. 本地缓存的特殊之处

当存在多个缓存设备时,好比客户端设备中有缓存,CDN 中也有缓存,此时就有两个缓存了。只要客户端本地有缓存,那么客户端就是一个一般意义上的 Web缓存设备,只不过客户端-服务器的距离几乎为 0 而已。网络

缓存的概念至关重要,缓存是一个设备。全部缓存相关的逻辑都是按照三个关键点来进行的,这三个关键点就是:客户端、缓存、源服务器,当同时存在客户端缓存和代理缓存时,其状况多是:测试

多个缓存设备

  • 本地缓存的特殊之处就在于在判断缓存未失效时能够直接使用缓存而不用发送 Request。

所以,请求一个 HTTP 时,先查询客户端本地的缓存,检查是否有缓存,若是有缓存再根据 Cache-Control: max-age=xxx 来判断缓存是否新鲜,若是足够新鲜,也就是第二次请求在 Cache-Control 时间内,此时能够直接使用本地保存的 Reponse + data,彻底不须要进行请求。优化

若是不够新鲜了,就不能使用本地缓存了,而是应该发起正式的请求。请求到了缓存服务器,缓存服务器会发送带条件的再验证请求到源站,也就是使用 If-Modified-Since 等方法来进行再验证。若是验证经过,缓存未过时,更新 Cache-Control/Expires 的值,从新计算时间,整合 Reponse 以后返回给客户端,返回的实体中不包含 data,此时状态码为 304。若是缓存失效,那么返回的状态码为200,实体中包含所有 data。ui

其时序图以下: url

客户端-本地缓存-代理缓存-源站

3、缓存过时

意义:当存在缓存时,使用过时验证的机制来验证缓存是否可使用,这一机制也有不少人称之为强制缓存spa

这一步通常是在本地缓存或者代理缓存中进行,经过 Cache-Control 或者是 Expires 进行验证。

1. Expires

老式的 HTTP1.0协议使用 Expires字段来表示文档的过时日期,好比:

Expires:Thu,15 Apr  2010  20:00:00  GMT
复制代码

**意义:**这个字段可使用一个组件的当前副本,直到指定的时间为止。

缺陷:

  1. 客户端和服务端的时钟必须严格一致;
  2. 时间到期以后服务器须要从新设置;

因此就有了第二种方式:

2. Cache-Control:max-age

Cache-Control:max-age 是对 Expires的优化处理,好比:

Cathe-Control:max-age=315360000
复制代码

**意义:**从请求开始在max-age时间均可以使用缓存,以外的使用请求。

如此,就能够消除 Expires 时间统一的限制。

**总结:**如今强制缓存通常都采用 Cache-Control: max-age=xxx 来设置。

备注:Cache-Control 还有不少其余的可选值,后文会介绍。

4、服务器再验证

意义: 即便缓存过时了,也不意味着缓存文件和原始服务器上的文件不一致,这只是意味着要进行时间核对来确认缓存是否仍然可使用。这个状况叫作服务器再验证。

**验证机制:**HTTP 容许客户端向服务器发送一个“条件GET”,根据条件判断,只有当服务器中的文档和缓存不同时,服务器才会在 Response 主体中包含所有的内容,不然返回 304,Response 中不包含资源。

条件语句有不少种,经常使用的有两种:

1. If-Modified-Since 和 Last-Modified

If-Modified-Since 客户端使用,在请求头中添加。Last-Modified 服务端使用,在响应头中返回。两个配合使用来验证资源是否真的发生了改变。若是改变了,状态码为200,响应主体中包含全部内容,若是为改变,状态码为304,响应实体中不包含主体,只包含头部。

举个栗子🌰:

第一次请求的 Response 以下:

Response

从图中能够看出,Response 中包含 Cache-Control: max-age=10,表示在10秒内能够直接使用缓存,超过10秒就须要进行再验证。同时 Response 中带有一个 Last-Modified:

第二次进行请求的请求头和响应头:

再验证经过

上图表示10秒后进行了缓存再验证且验证经过,因此返回的是304。

2. If-None-Match 和 ETag

**意义:**有些资源会周期性重写,可是内容却未发生变化,此时 If-Modified-Since 就不能知足要求,而是须要一个实体标签。

客户端记录服务端在响应头中的 ETag 并在请求通中使用 If-None-Match 字段提交给服务器。同理,若是 ETag 一致,表示缓存的资源在源站中未发生改变,因此此时会返回 304,表示缓存可用。不然就会返回 200,在返回的主体中包含完整的内容。同时,Response 中会返回最新的 ETag。

举个栗子,缓存再验证经过:

ETag缓存再验证经过

4. 强弱验证

**意义:**有些文档被修改了,可是修改的内容不重要,好比注释,此时须要一个强弱标签来告诉使用者,什么状况下缓存还能够继续使用。

仍然相似于 Git 上的代码管理。每次提交代码,都会在对应的分支上生成一个索引值,可是并非每次提交都会修改版本号的,更不是每次更新都会生成一个大的版本号。

缓存也存在这种状况,由于资源的某些无伤大雅的修改并不影响原先副本的继续使用,好比注释。因此存在强弱验证的状况:

**弱验证:**内容的主要含义发生变化时,弱验证器才会发生变化。 **强验证:**只要内容发生变化,强验证器就会发生变化。

例如:

Etag:w/"2.6"
If-None-Match:w/"2.6"
复制代码

不带 w/ 就是强验证,例如:

Etag:"2.6"
If-None-Match:"2.6"
复制代码

5. If-None-Match的多值状况

If-None-Match 能够有多个值,表示这些版本的副本在缓存中都存在,如图:

If-None-Match

6. 前后问题

由于缓存控制的缘由,对缓存的使用会分不少状况。好比 Cache-Control 为 no-cache 时,表示必须进行再验证经过后,才能使用缓存。而 Cache-Control 为 max-age=xxx 时表示在收到 Response 以后的这个时间内均可以使用缓存。

另外,除了 Cache-Control 对缓存的控制,还会有试探性过时的机制,所以缓存的使用与否的逻辑并非简单的 Yes or No,而是须要根据多重条件进行综合判断,后文会有存在 Cache-Control 和不存在 Cache-Control 状况下的常见逻辑。

所以,If-Modified-Since/If-None-Match 和 Last-Modified/ETag 二者的前后关系不肯定。其一种常见的做用机制是服务端返回 Last-Modified 字段,客户端进行缓存,若是须要进行缓存的再验证(好比max-age过时了),那么就将存储的值做为 If-Modified-Since 的值添加在请求头中发送给服务器。

7. 特别注意

  1. 若是服务器返回一个实体标签(ETag),HTTP/1.1,客户端就必须使用实体标签;
  2. 若是服务器只回送了一个 Last-Modified 值,客户端就可使用 If-Modified-Since 验证,非必须;
  3. 若是二者都提供了,那么就须要使用两种验证方案,这样就能够兼容 HTTP/1.0 和 HTTP/1.1,但不是必须;
  4. 若是客户端的头部中既包含实体标签又包含最后修改日期,那么服务端只有在两个条件都验证经过时才能返回 304;

如图:

再验证

再验证

正由于如此,才有了试探性过时的缓存策略。

5、缓存控制

1. Cache-Control之于代理缓存

  • no-store

缓存中不得存储任何关于客户端请求和服务端响应的内容。每次由客户端发起的请求都会下载完整的响应内容。

  • no-cache

代理缓存能够存储缓存,可是必须在和源站进行验证以后才能提供给客户端。

  • private

只能用于私有缓存(客户端缓存),中间人不能缓存,默认为private;

  • public

公共缓存,能够用于中间人(代理缓存、CDN);

  • must-revalidate

若是过时,那么必须验证后才能使用或者提供给客户端,比 no-cache 稍微宽松,no-cache 不论是否过时都要验证;

  • max-age

过时时间,从服务器将资源传来之时,资源处于新鲜状态的秒数;

  • pragma

是HTTP/1.0标准中定义的一个 header 属性,请求中包含Pragma 的效果跟在头信息中定义Cache-Control: no-cache相同,可是HTTP的响应头没有明肯定义这个属性,因此它不能拿来彻底替代HTTP/1.1中定义的Cache-control头。一般定义Pragma以向后兼容基于HTTP/1.0的客户端。

2. Cache-Control之于客户端

客户端也能够在请求中添加 Cache-Control 请求首部,完整的意义以下:

Cache-Control请求指令

其中最经常使用的有两种:

  • Cache-Control: no-cache + Pragma: no-cache

表示代理缓存必须对缓存进行验证,验证经过后才能提供缓存。Pragma 是为了支持 HTTP1.0;

  • Cache-Control: no-store

表示代理缓存不能提供缓存,且代理缓存中的的缓存资源应该删除;

6、试探性过时

1. 定义

若是响应中没有 Cache-Control 也没有 Expires ,那么缓存就能够计算出一个试探性最大使用期。

最大使用期的计算可使用任意算法,可是若是获得的最大试用期大于24小时,就应该想响应首部添加一个试探性过时,可是这种方式的使用不多,经常使用的是 LM-factor 算法。

2. LM-factor 算法

使用条件:

    1. 响应中没有 Cache-Control 也没有 Expires;
    1. 响应中存在 Last-Modified;

计算方法:

试探性最大使用期 = rate * (请求时间 - 最后修改时间)

举个栗子🌰:

LM-factor 算法

3. 特别提示

《HTTP权威指南》中特别指出:

试探性过时特别注意

而实际上,safari 就对试探性过时进行了实现。

7、iOS中的系统实现的缓存策略

iOS 中的 NSURLSession 中使用到了 NSURLRequestCachePolicy ,这个枚举就是 Apple 遵循 HTTP 协议,将 iPhone 做为一个本地缓存设备,实现了和协议对应的缓存逻辑,可是其逻辑使用到了试探性过时,其判断逻辑大体以下:

iOS中的NSURLRequestCachePolicy

验证:

iOS 中,会存在这样一种状况: 若是 Response 存在 ETag,Request头部中会自动添加 If-None-Matched,可是若是同时存在 Last-Modified,却不会主动添加If-Modified-Since,而是直接存储后使用,在第二次使用时不请求网络直接使用缓存,此时试探性过时机制生效,缓存未过时。

原始的响应头(省略了一些内容):

HTTP/1.1 200 OK
Date	Wed, 19 Feb 2020 08:09:14 GMT
Content-Type	image/jpeg
Server	openresty/1.11.2.5
Content-MD5	c6090671ef82012e7e71b6dc938dc706
ETag	51cf999237cf860b7fd92e6986fc4767
Last-Modified	Wed, 12 Feb 2020 12:01:36 Asia/Shanghai
复制代码

再次请求就抓不到包了,却获得了 200 的响应,且包含 data,此时就是试探性过时机制生效,直接使用的本地的 Reponse 和 data,并无进行请求。

其实,可使用 charles 断点,对这个 Response 进行几种操做:

  1. 去掉 Last-Modified
  2. 修改 Last-Modified 为和请求时间相近
  3. 添加 Cache-Control

1. 去掉Last-Modified

意义: Response 中只有 ETag,因此客户端下一次请求时不触发试探性过时,也不会触发强制缓存,而是直接请求进行缓存的再验证;

操做以下:

去掉Last-Modified ,而后,经过 charles 断点 修改响应头,去掉了 Last-Modified 字段,最终的响应头以下:

HTTP/1.1 200 OK
Date	Wed, 19 Feb 2020 08:09:14 GMT
Content-Type	image/jpeg
Server	openresty/1.11.2.5
Content-MD5	c6090671ef82012e7e71b6dc938dc706
ETag	51cf999237cf860b7fd92e6986fc4767
复制代码

再次进行请求, charles 可以抓到包,证实没有触发强制缓存,也没有触发试探性过时,请求头和响应头以下:

缓存再验证成功

由图可知:客户端发送了一个条件GET进行缓存验证,且验证成功。

2. 修改 Last-Modified 为和请求时间相近

由于不存在 Cache-Control 可是却存在 Last-Modified,因此 iOS 会触发试探性过时。

未修改以前这个值相对较老,试探性过时触发以后会判断短期内缓存不会过时,因此不会发送请求,直接使用缓存。

可是若是修改 Last-Modified 为和请求时间相近,那么试探性过时的计算结果为缓存很快就会过时,资源变更比较频繁,因此此时 iOS 会发送一个一个条件请求进行缓存再验证。

修改后的 Reponse 以下:

HTTP/1.1 200 OK
Date: Wed, 19 Feb 2020 09:06:27 GMT
Content-Type: image/jpeg
Content-Length: 3444
Connection: keep-alive
Server: openresty/1.11.2.5
Content-MD5: c6090671ef82012e7e71b6dc938dc706
ETag: 51cf999237cf860b7fd92e6986fc4767
Last-Modified: Wed, 19 Feb 2020 09:06:27 GMT
复制代码

再次进行请求的请求头和响应头以下:

试探性过时判断结果为缓存过时

3. 添加 Cache-Control

**意义:**添加 Cache-Control 后,就按照正常逻辑走了,也就是说不会触发试探性缓存了。

由于 Cache-Control 相对复杂,这里直接使用 max-age=5 和 max-age=36500 和 来做为示例;

修改后的响应头为:

HTTP/1.1 200 OK
Date: Wed, 19 Feb 2020 09:13:05 GMT
Content-Type: image/jpeg
Content-Length: 3444
Connection: keep-alive
Server: openresty/1.11.2.5
Content-MD5: c6090671ef82012e7e71b6dc938dc706
Cache-Control: max-age=5
ETag: 51cf999237cf860b7fd92e6986fc4767
Last-Modified: Wed, 12 Feb 2020 12:01:36 Asia/Shanghai
复制代码

过5秒以后再次请求确定不会触发强制缓存,而是在强制缓存失效以后,进行正常的进行缓存再验证:

强制缓存判断缓存失效后再验证成功

同理,若是 max-age=36500,短时间内强制缓存生效,确定是直接使用本地缓存而不进行请求。

附上测试代码:

// 图片
NSURL *url = [NSURL URLWithString:@"http://cms-bucket.ws.126.net/2020/0212/51cf9992j00q5kluo00bmc000tj00tjc.jpg?imageView&thumbnail=140y88"];

NSMutableURLRequest *request = [NSMutableURLRequest new];
request.HTTPMethod = @"GET";
request.URL = url;

// 查询是否有缓存
NSCachedURLResponse *cacheReponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:request];
if (cacheReponse) {
    NSLog(@"本地存在缓存");
} else {
    NSLog(@"本地无缓存");
}

NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    NSLog(@"%@",request.allHTTPHeaderFields);
    NSHTTPURLResponse *httpReponse = (NSHTTPURLResponse *)response;
    NSLog(@"statusCode:%li",httpReponse.statusCode);

     if (data.length > 0) {
        NSLog(@"响应有数据");
    } else {
        NSLog(@"响应无数据");
    }
}];
[task resume];
复制代码

总结

几个知识点再总结下:

总结

结尾

正常来说,iOS 中使用默认的缓存机制,而后服务端按照 HTTP/1.1 协议正确配置好缓存过时字段(Expires/Cache-Control)和条件验证字段(If-Modified-Since/If-None-Match),基本上就能知足大部分的需求,并且将缓存更新与否的决定权交给了服务端,也就是 H5 页面能够控制 App 中的页面是否更新,并不用发包。

更多文章
相关文章
相关标签/搜索