[iOS] iOS中URLRequest的缓存策略

3,800 阅读16分钟

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协议规则进行检查,来决定是让客户端使用缓存还是使用服务器下发的资源。

    • 服务器的两种响应头:
      1. 状态码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
        
      2. 状态码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
        
    • 服务器响应头里与HTTP协议缓存策略相关的字段:
      1. Cache-Control: max-age = X (max-age=x 是告诉客户端x秒之内不要再发起请求了,就用你的缓存就OK了,换句话说,如果服务器告诉客户端max-age=100,客户端在100s之内再去请求,是不会发起真正的网络请求的,客户端的网络层框架会自动返回状态码200,上一次的缓存数据)
      2. Last-Modified: Tue, 31 Dec 2019 14:57:28 GMT(这个字段是告诉客户端,这个资源最后一次更新的时间,让客户端保存好,下一次请求的时候,在请求头里面带上这个值,请求头的那个字段就是If-Modified-Since,这里的规则是这样的,如果请求头里的If-Modified-Since时间点早于服务器的Last-Modified时间点,服务器会返回200,让客户端需要更新最新资源,如果反过来,或者相同,服务器会下发304,让客户端使用缓存。)
      3. 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-MatchETag一致的,发现Last-ModifiedIf-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当中我发现直接输入资源地址,它好像会忽略过期时间一样,都会发起真正的网络请求,我看了小半天,我终于发现了,这个问题出在哪了,下面我来讲一下。

  1. 移动端下: 使用URLRequest的默认缓存策略,缓存策略使用协议本身的缓存策略,设置超时时间工作正常。
  2. Web端: 使用标签或者XHR发起的网络请求,设置超时时间工作正常。
  3. Web端: 使用浏览器直接访问资源,发现即使服务器设置了超时时间,还是会发起网络请求。

总结下来: 只有 3 显示的不正常,问题可能就是因为直接在浏览器里面输入资源地址去访问资源,忽略超时时间,可能由于某些原因吧,所以每次才都会发起网络请求,而且Chrome和Safri的表现行为还不一样,所以我们如果想验证HTTP协议的缓存策略其实可以忽略掉3,验证12即可。

二,iOS下,URLRequest.CachePolicy和URLCache

上面我们主要介绍了一些事先需要了解的知识,比如iOS里面的URLRequest和URLRequest.CachePolicy使用范围,还有最重要,也是最复杂的HTTP协议缓存策略。

0. CachePolicy 与 URLCache 的关系

CachePolicy:顾名思义这个是缓存策略的意思,我下面会仔细说它,这里我们知道它代表一种缓存的策略就OK。

URLCache: 这个其实就是我们缓存的对象,也就是我们使用了某种缓存策略,把内容存储的地方和配置存储在哪地方,等等,或者说管理URL缓存的对象。

小结: CachePolicy 用于选择一种缓存策略,URLCache管理和设置缓存对象。

1. 有哪些地方可以设置 CachePolicy

首先先了解 CachePolicy 有哪些策略可选:

  • useProtocolCachePolicy // 使用协议缓存,比如你的URLHTTP协议的,那就是使用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 疑问🤔️? 同时设置,URLSessionCondigurationURLRequest 的缓存策略,系统到底该用哪个呢?

A 答案: URLRequest 的缓存策略会覆盖掉 URLSessionCondiguration 的缓存策略。

2. 有哪些地方可以设置 URLCache

区分 URLCacheNSCache nshipster.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. 几种特殊场景

  1. 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()
    
  2. 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一起实践更容易理解。

参考: blackpixel.com/writing/201…