
Example 项目地址: github.com/zColdWater/… 下载Demo,结合Demo一起实践更容易理解。
一,准备工作
在开始之前,我们需要清楚下面的一些问题,才方便我们后面的讲解。
1. URLRequest 涉及的范围
我们一提到URLRequest,我相信很多国内的开发者,首先就会联想到,HTTP请求,然后木有别的了。 但是其实 URLRequest是一个很大的概念,它不只服务于HTTP协议,它还服务于 其他应用协议,比如File协议,Data协议,自定义协议等等。 要么苹果公司为什么不叫它HTTPURLRequest呢? 问题就在于我们平时最常接触的就是HTTP协议,用来请求服务端的数据用来展示。
2. URLRequest.CachePolicy 涉及的范围
通过上面的文章我们清楚了 URLRequest 服务很多协议,那么URLRequest.CachePolicy的范围是什么呢,很明显,和URLRequest一样,这个缓存策略也包含上面这些协议,当然 我也不清楚其他协议的缓存策略是什么样子的,比如File协议,或则别的。 但是我很清楚,我们常用的HTTP协议的缓存协议,这个后面再讲,这里清楚的是,这个缓存策略支持很多协议,我们的HTTP协议有着自己的缓存策略。
3. HTTP协议的缓存策略
我之前转了一篇台湾作者关于HTTP协议的缓存策略的文章,文章地址是: http://47.99.237.180:8080/articles/2019/11/18/1574050998351.html
那么HTTP协议的缓存策略是什么呢?
Note: 首先我们需要清楚的是,
HTTP协议的策略是需要客户端和服务端配合完成的。也就是如果这个策略想要完成,需要双方都有动作,并且客户端需要完全配合才行。
我用最直白的话来概述这个原理:
-
首先
客户端第一次访问服务器某一个资源,并且服务器和客户端协商好,我们都用标准的HTTP缓存协议。 -
因为是第一次,
客户端通过URL来查找,发现本地没有缓存,直接向服务器发起一个HTTP协议的网络请求。客户端的请求头,例如:GET /EC6/poster-share-invite.png HTTP/1.1 Host: fep-sit.nioint.com:5418 Accept: */* User-Agent: SessionDownload/1 CFNetwork/1107.1 Darwin/19.0.0 Accept-Language: en-us Accept-Encoding: gzip, deflate Connection: keep-alive这其实就是一个普通的网络请求,请求头也是客户端默认的,没有特殊设置。
-
服务器接到了某一个客户端的发来的请求,然后做的直接就看一下这个请求头有没有提供HTTP协议缓存的相关字段,然后根据HTTP协议缓存规则,来判断是否返回状态码304让客户端用缓存,还是返回状态码200,让客户端使用服务器返回的新资源,服务器检查请求头的相关字段如下:If-None-Match: W/"20f9a-16f5c76370d" 这个字段你可以理解为这个资源的唯一Hash值,有点像MD5或者SHA1等,反正就是一个唯一标识啦,资源如果有变动,它一定就会有变动,并且这个值是从上一次服务器返回的响应头里面的Etag字段取来的。因为我们客户端是第一次请求,所以没有从之前的服务器响应里面拿到这个值,所以请求头就没有这个字段。If-Modified-Since:Tue, 31 Dec 2019 14:57:28 GMT,这个字段表示的是最后一次资源更改的时间,同If-None-Match也是从上一次的服务器响应头中拿到,从Last-Modified字段取的。因为第一次请求,所以没有获取到上一次响应头的字段,也就没有带上。
-
服务器开始根据
HTTP协议规则进行检查,来决定是让客户端使用缓存还是使用服务器下发的资源。- 服务器的两种响应头:
- 状态码200(告诉客户端,不要使用缓存,用我给你的新资源)
HTTP/1.1 200 OK X-Powered-By: Express Accept-Ranges: bytes Cache-Control: public, max-age=0 Last-Modified: Tue, 31 Dec 2019 14:57:28 GMT ETag: W/"20f9a-16f5c76370d" Content-Type: image/gif Content-Length: 135066 Date: Wed, 01 Jan 2020 01:56:35 GMT Proxy-Connection: keep-alive - 状态码304(告诉客户端你用自己的缓存即可)
这里注意的是,在iOS当中,你不需要亲自处理
304的情况,如果你使用了默认的缓存策略,也就是使用HTTP协议本身的缓存策略,系统的网络框架比如URLSession或者URLConnection会自动的将这个304处理成200,这样方便了开发者逻辑处理,开发者只需要知道 资源获取成功,就可以了。HTTP/1.1 304 Not Modified X-Powered-By: Express Accept-Ranges: bytes Cache-Control: public, max-age=0 Last-Modified: Tue, 31 Dec 2019 14:57:28 GMT ETag: W/"20f9a-16f5c76370d" Date: Wed, 01 Jan 2020 01:59:25 GMT Proxy-Connection: keep-alive
- 状态码200(告诉客户端,不要使用缓存,用我给你的新资源)
- 服务器响应头里与HTTP协议缓存策略相关的字段:
Cache-Control: max-age = X (max-age=x 是告诉客户端x秒之内不要再发起请求了,就用你的缓存就OK了,换句话说,如果服务器告诉客户端max-age=100,客户端在100s之内再去请求,是不会发起真正的网络请求的,客户端的网络层框架会自动返回状态码200,上一次的缓存数据)Last-Modified: Tue, 31 Dec 2019 14:57:28 GMT(这个字段是告诉客户端,这个资源最后一次更新的时间,让客户端保存好,下一次请求的时候,在请求头里面带上这个值,请求头的那个字段就是If-Modified-Since,这里的规则是这样的,如果请求头里的If-Modified-Since时间点早于服务器的Last-Modified时间点,服务器会返回200,让客户端需要更新最新资源,如果反过来,或者相同,服务器会下发304,让客户端使用缓存。)ETag: W/"20f9a-16f5c76370d"(这个字段告诉客户端,这个值是这个资源的唯一id,如果服务器上面有新资源,我们会更新这个值,客户端要保存好,下次请求的时候带上,下次请求头里面这个If-None-Match字段就是保存的上次响应头里面的Etag字段。它的规则是,如果客户端请求头中的If-None-Match值不与服务器里面的Etag一致,就返回200,让客户端使用新资源,只有当相等的情况,会返回304,让客户端使用缓存。)
- 服务器的两种响应头:
-
服务器检查完请求头发现这个客户端没有带上资源缓存信息,那么服务器就认为客户端不想使用
HTTP协议缓存策略,返回200,把资源也一同返回。HTTP/1.1 200 OK X-Powered-By: Express Accept-Ranges: bytes Cache-Control: public, max-age=0 Last-Modified: Tue, 31 Dec 2019 14:57:28 GMT ETag: W/"20f9a-16f5c76370d" Content-Type: image/gif Content-Length: 135066 Date: Wed, 01 Jan 2020 01:56:35 GMT Proxy-Connection: keep-alive -
客户端第一次拿到资源后,先将服务器告诉自己的缓存信息,保存起来,
Cache-Control: public, max-age=0读取一下max-age,发现值等于0,这是告诉我应该每次都发真正的请求,然后再存一下Last-Modified上次更新的时间,再存一下ETag,这个告诉我们资源的唯一id。 -
然后
客户端开始发起第二次请求,请求头如下:GET /demo.gif HTTP/1.1 Host: 152.136.154.126:3000 If-None-Match: W/"20f9a-16f5c76370d" Accept: */* If-Modified-Since: Tue, 31 Dec 2019 14:57:28 GMT User-Agent: SessionDownload/1 CFNetwork/1107.1 Darwin/19.0.0 Accept-Language: en-us Accept-Encoding: gzip, deflate Connection: keep-alive
可以看到,客户端将缓存资源的信息带上来了,在If-None-Match,If-Modified-Since。
服务器再次接到这个请求头,和自己的对资源的信息对比,发现If-None-Match和ETag一致的,发现Last-Modified和If-Modified-Since一致的,那么客户端的资源就和服务器是一致的,我应该告诉客户端304状态码,让客户端使用缓存就可以了。 响应头如下:HTTP/1.1 304 Not Modified X-Powered-By: Express Accept-Ranges: bytes Cache-Control: public, max-age=0 Last-Modified: Tue, 31 Dec 2019 14:57:28 GMT ETag: W/"20f9a-16f5c76370d" Date: Wed, 01 Jan 2020 03:02:57 GMT Proxy-Connection: keep-alive
自此,一次完整的HTTP协议缓存策略应用完成,我们可以看到,客户端第一次发起请求,第二次发起请求,请求头里面的变化。和服务器如何对请求头做出的校验和响应。
我知道还有
Expire字段,也是缓存相关的,但是HTTP1.1之后设置Cache-Control里面的max-age,可以覆盖它,如果他们同时存在,所以这里不再概述,如果你感兴趣,可以WIKI,或者 http://47.99.237.180:8080/articles/2019/11/18/1574050998351.html
希望我可以说清楚这个过程,因为下面iOS下面的网络框架就会涉及到。 这个是HTTP协议的缓存策略,意味着,只要使用HTTP协议的客户端,不管是浏览器,还是移动端,还是其他,都使用。
即使现在不是特别清楚也没关系,我会在文章中附上,整个过程的Demo,可以运行Demo,来了解每一步。
4. 出了一个小插曲
在验证过程中除了一些小插曲,比如我已经为服务器的静态资源,设置资源的过期时间,在响应头当中,
Cache-Control: max-age=xxx,发现在移动端上,是OK的,在xxx秒之内再次访问,真的没有发起网络请求,但是在Chrome当中我发现直接输入资源地址,它好像会忽略过期时间一样,都会发起真正的网络请求,我看了小半天,我终于发现了,这个问题出在哪了,下面我来讲一下。
- 移动端下: 使用URLRequest的默认缓存策略,缓存策略使用协议本身的缓存策略,设置超时时间工作正常。
- Web端: 使用
标签或者XHR发起的网络请求,设置超时时间工作正常。 - Web端: 使用浏览器直接访问资源,发现即使服务器设置了超时时间,还是会发起网络请求。
总结下来: 只有 3 显示的不正常,问题可能就是因为直接在浏览器里面输入资源地址去访问资源,忽略超时时间,可能由于某些原因吧,所以每次才都会发起网络请求,而且Chrome和Safri的表现行为还不一样,所以我们如果想验证HTTP协议的缓存策略其实可以忽略掉3,验证1和2即可。
二,iOS下,URLRequest.CachePolicy和URLCache
上面我们主要介绍了一些事先需要了解的知识,比如iOS里面的URLRequest和URLRequest.CachePolicy使用范围,还有最重要,也是最复杂的HTTP协议缓存策略。
0. CachePolicy 与 URLCache 的关系
CachePolicy:顾名思义这个是缓存策略的意思,我下面会仔细说它,这里我们知道它代表一种缓存的策略就OK。
URLCache: 这个其实就是我们缓存的对象,也就是我们使用了某种缓存策略,把内容存储的地方和配置存储在哪地方,等等,或者说管理URL缓存的对象。
小结: CachePolicy 用于选择一种缓存策略,URLCache管理和设置缓存对象。
1. 有哪些地方可以设置 CachePolicy
首先先了解 CachePolicy 有哪些策略可选:
useProtocolCachePolicy// 使用协议缓存,比如你的URL是HTTP协议的,那就是使用HTTP协议的缓存策略,就是我上面讲的,根据请求和响应头的关键字,进行缓存的处理。 这也是(默认的策略)。reloadIgnoringLocalCacheData// 完全忽略本地缓存,直接从服务器拿资源。reloadIgnoringLocalAndRemoteCacheData// 这个实例是未实现的,你不应该使用reloadIgnoringCacheData// 这个选项已经被 reloadIgnoringLocalCacheData 选项所替代了。returnCacheDataElseLoad// 如果本地有缓存就使用缓存,不管HTTP协议上缓存上的那些max-age,expire 过期时间等,没有缓存再去远端请求拿数据。这个协议存在的问题就是,如果使用HTTP协议的缓存策略,服务器没办法告诉它,它手头的这份资料是旧的。returnCacheDataDontLoad// 如果本地有缓存就使用缓存,本地没有缓存就请求失败,类似离线模式。reloadRevalidatingCacheData// 这个实例是未实现的,你不应该使用它。
其实苹果给的默认策略,一点毛病都没有,根据HTTP协议的缓存策略来配置是最好的,有缓存使用缓存,没有缓存,或者缓存过期,来请求服务器的资源。 也就是你根本毛都不用去设置,但是我们经常会被问及,为啥我服务器换了图片,你移动端iOS不更新呢,如果你是默认策略,我可以很负责的告诉你,你可以狠狠的怼回去,你懂不懂HTTP协议的缓存策略,我这是官方的实现,而不是,不明道理,萎萎诺诺,好,我改成一切都从服务器获取。 不懂的哥们还以为你技术不过关呢,不过说句实话,确实许多人真的不太清楚
HTTP协议的缓存策略。
有哪些地方可以设置缓存策略URLRequestCache呢?
其实看名字你就应该知道,这东西肯定是给URLRequest设置的,我每一个网络请求,都会有一个URLRequest,但是,为了方便,苹果会有两个地方给你设置这个策略的地方,分别是:
- URLSessionConfiguration: 所有通过
URLSession发出去的URLRequest都会带上这个策略。let url = URL(string: "http://152.136.154.126:3002/demo.gif")! var request = URLRequest(url: url) let condig = URLSessionConfiguration.default // 这 这 这 ⬇️ 这 这 这 ⬇️ 这 这 这 ⬇️ 这 这 这 ⬇️ condig.requestCachePolicy = .reloadIgnoringLocalCacheData let session = URLSession(configuration: condig) - URLRequest: 设置这个
URLRequest的缓存策略,这个就没什么说的了。let url = URL(string: "http://152.136.154.126:3002/demo.gif")! var request = URLRequest(url: url) // 这 这 这 ⬇️ 这 这 这 ⬇️ 这 这 这 ⬇️ 这 这 这 ⬇️ request.cachePolicy = .returnCacheDataDontLoad let condig = URLSessionConfiguration.default let session = URLSession(configuration: condig)
Q 疑问🤔️? 同时设置,URLSessionCondiguration 和 URLRequest 的缓存策略,系统到底该用哪个呢?
A 答案: URLRequest 的缓存策略会覆盖掉 URLSessionCondiguration 的缓存策略。
2. 有哪些地方可以设置 URLCache
区分
URLCache和NSCachenshipster.com/nsurlcache/ 这俩东西不是一回事哈,看名字你也知道了,一个是专门缓存URL的类,一个是类似Map,字典的存储结构,用来缓存内存对象的。
上面也说了,URLCache 这个对象,其实是用于管理URL的缓存的,比如放在哪里呀,大小设置多少呀。
都哪些地方可以设置呢? 有俩地方可以设置,分别是:
-
URLSessionConfiguration: 单独为这个
URLSession配置缓存对象,大小,路径等,如果单独为URLSessionConfiguration配置了缓存对象,由这个URLSession发出去的URLRequest,都会使用这个缓存配置,不会使用全局的缓存对象。let condig = URLSessionConfiguration.default condig.requestCachePolicy = .reloadIgnoringLocalCacheData let cache = URLCache(memoryCapacity: 4 * 1024 * 1024, diskCapacity: 20 * 1024 * 1024) condig.urlCache = cache -
URLCache: 类方法提供了全局的
URLCache配置,当URLSessionCondiguration没有另外设置的情况下,会使用全局的缓存作为自己的缓存。URLCache.shared = URLCache(memoryCapacity: 0, diskCapacity: 0, diskPath: nil)
Q 疑问🤔️? 如果同时设置全局缓存和URLSessionCondiguration缓存,系统有哪个缓存?
A 回答: 肯定使用URLSessionCondiguration的缓存,忽略全局缓存。
3. 几种特殊场景
-
URLSessionConfiguration 的缓存对象,空间设置为0。
let url = URL(string: "http://152.136.154.126:3002/demo.gif")! var request = URLRequest(url: url) let condig = URLSessionConfiguration.default // 为这个Session单独设置缓存,并且空间都为0,因为都是用默认缓存策略,也就是使用`HTTP协议`缓存,但是本地缓存没有设置空间,所以每次都会从服务器拿去最新的资料。 let cache = URLCache(memoryCapacity: 0, diskCapacity: 0) let session = URLSession(configuration: condig) let task = session.dataTask(with: request) { (data, resp, error) in let httpResp = resp as! HTTPURLResponse print("response (StatusCode):\(httpResp.statusCode)") DispatchQueue.main.async { let httpResponse = resp! as! HTTPURLResponse for (key,value) in httpResponse.allHeaderFields { print("\(key):\(value)") } self.imageView.image = UIImage.gifImageWithData(data: data! as NSData) } } task.resume() -
URLCache全局缓存对象,空间设置为0。
// 因为URLSessionCondiguration的缓存没有特殊设置,所以使用全局缓存,又因为全局缓存空间设置成0,又因为是默认缓存策略,所以每次都从服务器去拿最新资料。 URLCache.shared = URLCache(memoryCapacity: 0, diskCapacity: 0, diskPath: nil) let url = URL(string: "http://152.136.154.126:3002/demo.gif")! var request = URLRequest(url: url) let condig = URLSessionConfiguration.default let session = URLSession(configuration: condig) let task = session.dataTask(with: request) { (data, resp, error) in let httpResp = resp as! HTTPURLResponse print("response (StatusCode):\(httpResp.statusCode)") DispatchQueue.main.async { let httpResponse = resp! as! HTTPURLResponse for (key,value) in httpResponse.allHeaderFields { print("\(key):\(value)") } self.imageView.image = UIImage.gifImageWithData(data: data! as NSData) } } task.resume()
三,关于WKWebView和UIWebView里面的请求缓存是怎样的。
关于WebView这块是这样的,你设置的缓存策略,是页面里面的标签的缓存策略,也就是,你虽然设置了缓存策略是: 不使用本地缓存,只是HTML加载的那些标签使用这个策略,比如<img>,但是如果在HTML的JS发起的XHR,或者Fetch请求,使用的还是 默认缓存策略,你不会改变到他们。 这是我用网络拦截验证到的结果,Demo在文章开头和结尾都有。
拦截工具 项目地址: github.com/zColdWater/… 。
一,如果你想让这个网站的标签都使用默认的缓存策略,你就不需要另外设置。 也就是使用HTTP协议的缓存策略。
let url = URL(string: "https://www.baidu.com/")
var request = URLRequest(url: url!)
webview.load(request)
二,如果你想让这个网站的资源标签使用特定的缓存策略来请求,你可以这样处理。
let url = URL(string: "https://www.baidu.com/")
let request = URLRequest(url: self.url!, cachePolicy: .reloadIgnoringLocalCacheData)
webview.load(request)
小结: 如果文章不够直观,可以下载Example项目,运行查看哦。
四,总结
其实对于iOS当中的URLRequest缓存,苹果的默认策略就是非常合理的选择,但是想要合理的使用这个默认策略,你需要了解HTTP协议的缓存策略,对于WebView的缓存策略我们也说明了,我希望可以帮到之前对这个缓存策略不清楚的童鞋,不要遇到问题一股脑就设置成不使用缓存了。 合理的使用缓存是最好的选择。
希望我可以说的清楚明白,如有不准确或者不对的地方,希望大家指正。
Example 项目地址: github.com/zColdWater/… 下载Demo,结合Demo一起实践更容易理解。
