NSURLProtocol

URL Loading System默认支持http、https、ftp、file和data协议,但是它同样也支持注书籍身的类来支持更多运用层网络协议。
详细而言NSURLProtocl可以实现以下需求(包含但不限):

重定向网络要求(或进行域名转化、拦截等,例如:netfox)

忽略某些要求,利用本地缓存数据

php304缓存基于NSURLCache的缓存实现 Webpack

自定义网络要求的返回结果 (比如:GYHttpMocking)

进行网络全局配置

NSURLProtocol类似中间人设计,将网络求细节供应给开拓者,而又以一种优雅的办法暴漏出来。
NSURLProtocol的定义更像是一个URL协议,只管它继续自NSObject却不能直策应用,利用时须要自定义一个协议继续NSURLProto

办理DNS挟制

随着互联网的发展,运营商挟制这些年逐渐被大家所提及,常见的挟制包括HTTP挟制和DNS挟制。
对付HTTP挟制更多的是修改网络相应加入一些脚本广告之类的内容,办理这个问题只要利用https加密要求交互内容;而对付DNS挟制则更加可恶,在DNS解析时让要求重新定向到一个非预期IP从而达到内容修改。

办理DNS挟制普遍的做法便是将URL从域名更换成IP,这么一来访问内容并不经由运营商的Local DNS到达指定的做事器,因此也就避免了DNS挟制问题。
当然,域名和IP的对应常日通过做事器下发担保获取最近的资源节点(当然也可以采取一些收费的HTTPDNS做事),不过这样一来操作却不得不依赖于详细要求,而利用自定义NSURLProtocol的办法则可以彻底办理详细依赖问题,不管是利用NSURLConnection、NSURLSession还是UIWebView(WKWebView有所不同),所有的更换操作都可以统一进行掌握。

下面的demo中自定义协议MyURLProtocol实现了将域名转化成IP进行要求的过程:

值得把稳的是利用URLSession进行网络要求时如果利用的不是默认会话(URLSession.shared)须要在URLSessionConfiguration中指定protocolClasses,这样自定义URLProtocol才能进行处理。
在MyURLProtocol的startLoading方法内同样发起了URL要求,如果此时利用了URLSession.shared进行网络要求则同样会造成MyURLProtocol调用,如此会引起循环调用。
考虑到startLoading方法能可能是NSURLConnnection实现,安全起见在MyURLProtocol内部利用URLProtocol.setProperty(true, forKey: MyCacheURLProtocolTagKey, in: newRequest)来标记一个要求,调用前利用URLProtocol.property(forKey: MyCacheURLProtocolTagKey, in: request)判断当前要求是否已经标记,如果已经标记则视为同一要求不再处理,从而避免同一个要求循环调用。

NSURLProtocol缓存

无论是NSURLConnection、NSURLSession还是UIWebView、WKWebView默认都是有缓存设计的(利用NSURLCache),不过这要合营做事器端response header利用,对付有缓存的页面(或者API接口),当缓存过期后,默认情形下(NSURLRequestUseProtocolCachePolicy)碰着同一个要求常日会发出一个header中包含If-Modified-Since的要求到做事器端验证,如果内容没有过期则返回一个不含有body的相应(Response code为304),客户端利用缓存数据,否则重新返回新的数据。

由于WKWebView默认有一段韶光的缓存,在第一次缓存相应后过一段韶光才会进行缓存要求检讨(缓存过期后才会发送包含If-Modified-Since的要求检讨)。
不过它做不到完备的离线阅读(只管在一定韶光内不须要检讨),而且无法做到缓存细节的掌握。

下面大略利用NSURLProtocol来实现WKWebView的离线缓存功能,不过把稳WKWebView默认仅仅调用NSURLProtocol的canInitWithRequest:方法,如果要真正利用NSURLProtocol进行缓存还必须利用WKBrowsingContextController的registerSchemeForCustomProtocol进行注册,但它是私有工具,须要动态设置。
下面的demo中大略实现了WKWebView的离线缓存功能,这样有碰着访问过的资源纵然没有网络也同样可以访问(当然,示例紧张用以解释缓存的事理,实际开拓中还有很多问题须要思考,比如说缓存过期机制、磁盘缓存保存办法等)。

NSURLCache

事实上,无论是NSURLConnection、URLSession还是UIWebView、WKWebView默认都是会利用缓存的(把稳WKWebView的缓存配置是从iOS 9.0开始供应的,但是iOS 8.0中也同样包含缓存设计,不过没有供应缓存配置接口)。
而NSURLConnection、NSURLSession和UIWebView默认都会利用NSURLCache,所有经由他们要求的数据都将被NSURLCache处理。
NSURLCache不仅供应了内存和磁盘缓存办法,还有完善的缓存策略可配置。
比如利用NRURLSession进行网络要求,就可以通过URLSessionConfiguration指定独立的URLCache(如果设置为nil则不再利用缓存缓存策略),通过URLSessionConfiguration的requestCachePolicy属性指定详细的缓存策略。

缓存策略CachePolicy

useProtocolCachePolicy:默认缓存策略,对付特定URL利用网络协议中实现的缓存策略。

reloadIgnoringLocalCacheData(或者reloadIgnoringCacheData):不该用缓存,直接要求原始数据。

returnCacheDataElseLoad:无论缓存是否过期,有缓存则利用缓存,否则重新要求原始数据。

returnCacheDataDontLoad:无论缓存是否过期,有缓存则利用缓存,否则视为失落败,不会重新要求原始数据。

实在对付多数开拓者而言默认缓存策略才是我们最关心的,这就有必要弄清HTTP的要乞降相应是如何利用headers来进行元数据交流的(无论是NSURLConnection还是NSURLSession都支持多种协议,这里重点关注HTTP、HTTPS)。

要求头信息 Request cache headers

If-Modified-Since:与相应头Last-Modified相对应,其值为末了一次相应头中的Last-Modified。

If-None-Match:与相应头Etag相对应,其值为末了一次相应头中的Etag

相应头信息 Response cache headers

Last-Modified:资源最近修正韶光

Etag:(Entity tag缩写)是要求资源的标识符,紧张用于动态天生、没有Last-Modified值的资源。

Cache-Control:缓存掌握,只有包含此设置可能利用默认缓存策略。
可能包含如下选项: max-age:缓存韶光(单位:秒)。
public:可以被任何区缓存,包括中间经由的代理做事器也可以缓存。
常日不会被利用,由于 max-age本身已经表示此相应可以缓存。
private:只能被当前客户端缓存,中间代理无法进行缓存。
no-cache:必须与做事器端确认相应是否发生了变革,如果没有变革则可以利用缓存,否则利用新要求的相应。
no-store:禁止利用缓存

Vary:决定要求是否可以利用缓存,常日作为缓存key值是否唯的确定成分,同一个资源不同的Vary设置会被作为两个缓存资源(把稳:NSURLCache会忽略Vary要求缓存)。

把稳:Expires是HTTP 1.0标准缓存掌握,不建议利用,请利用Cache-Control:max-age代替,类似的还有Pragma:no-cache和Cache-Control:no-cache。
此外,Request cache headers中也是可以包含Cache-Control的,例如如果设置为no-cache则解释这次要求不要利用缓存数据作为相应。

默认缓存策略下当客户端发起一个要求时首先会检讨本地是否包含缓存,如果有缓存则检讨缓存是否过期(通过Cache-Control:max-age或者Expires判断),如果没有过期则直策应用缓存数据。
如果缓存过期了,则发起一个要求给做事器端,此时做事器端比拟资源Last-Modified或者Etags(二者都存在的情形下下如果有一个不同则认为缓存已过期),如果不同则返回新数据,否则返回304 Not Modified连续利用缓存数据(客户端可以连续利用\"大众max-age\"大众秒缓存数据)。
这个过程中客户端发送不发送要求紧张看max-age是否过期,而过期后是否连续利用缓存则须要重新发起要求,做事器端根据情形关照客户端是否可以连续利用缓存(返回结果可能是200或者304)。

清楚了默认网络协议缓存干系的设置之后要利用默认缓存就比较大略了,常日对付NSURLSession你不做任何设置,只要做事器端相应头部加上Cache-Control:max-age:xxx就可以利用缓存了。
下面Demo中演示了如何利用NSURLSession通过max-age进行为期60s的缓存,运行会创造在第一次要求之后60s内不会进行再次要求,60s后才会发起第二次要求。

做事器端default-cache.php内容如下:

对应的要乞降相应头信息如下(做事器端设置缓存60s):

当然,合营做事器端利用缓存是一种不错的方案,自然官方设计时也是希望尽可能利用默认缓存策略。
但很多时候做事器端出于其他缘故原由考虑,或者说或客户端须要自定义缓存策略时还是有必要进行手动缓存管理的。
比如说如果做事器端根本没有设置缓存过期韶光或者做事器端根本无法获知用户何时清理缓存、何时利用缓存这些详细逻辑等都须要客户端自行制订缓存策略。

对付NSURLConnnection而言可以通过- (NSCachedURLResponse )connection:(NSURLConnection )connection willCacheResponse:(NSCachedURLResponse )cachedResponse进行二次缓存设置,如果此方法返回nil则不进行缓存,默认不实现这个代理则会走默认缓存策略。
而URLSessionDataDelegate也有一个类似的方法func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, completionHandler: @escaping (CachedURLResponse?) -> Swift.Void),利用和NSURLConnection是类似的,不同的是dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Swift.Void)等一系列带有completionHandler回调的方法并不会走代理方法,以是这种情形下func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, completionHandler: @escaping (CachedURLResponse?) -> Swift.Void)也是无法利用的。

无论URLSession走缓存干系的代理,还是通过completionHandler进行回调,默认都会利用NSURLCache进行缓存。
例如下面Demo3中的示例2、3都打印出了默认的缓存信息,不过如果做事器端不进行缓存设置的话(header中设置Cache-Control),默认情形下NSURLSession是不会利用缓存数据的。
如果将缓存策略设置为优先考虑缓存利用(例如利用:.returnCacheDataElseLoad),则可以看到下次要求不会再发送要求,Demo3中的示例4演示了这一情形。
不过一旦如此设置之后往后想要更新缓存就变得困难了,由于只要不清空缓存或超过缓存限定,缓存数据就一贯存在,而且在运用中随时换切换缓存策略本钱也并不低。
因此,要合理利用系统默认缓存的出发点还是该当着眼在默认的基于网络协议的缓存设置上。

不过这样一来缓存的掌握逻辑就上升为办理缓请安题的重点,比如说一个API接口设计多数情形下可以缓存,但是一旦用户修正了部分信息则希望及时更新利用最新数据,但是缓存不过期做事器端纵然很理解客户端设计也无法做到逼迫更新缓存,因此客户端就不得不自行掌握缓存。
那么能不能逼迫NSURLCache利用网络协议缓存策略呢,实在也是可以的,对付做事器端没有添加cache headers掌握的相应只须要添加上相应的缓存掌握即可。
Demo3的示例5解释了这一点。

缓存设计

从前面对于URL Loading System的剖析可以看出利用NSURLProtocol或者NSURLCache都可以做客户端缓存,但NSURLProtocol更多的用于拦截处理。
选择URLSession合营NSURLCache的话,则对付接口调用方有更多灵巧的掌握,而且默认情形下NSURLCache就有缓存,我们只要操作缓存相应的Cache headers即可,因此后者作为我们优先考虑的缓存方案。
鉴于图虫客户端利用Alamofire作为网络库,因此下面结合Alamofire实现一种相对大略的缓存方案。

根据前面的思路,最早还是想从URLSessionDataDelegate的缓存设置方法入手,而且Alamofire确实对付每个URLSessionDataTask都留有缓存代理方法的回调入口,但查看源码创造这个入口dataTaskWillCacheResponse并未对外开拓,而如果直接在SessionDelegate的回调入口dataTaskWillCacheResponseWithCompletion上进行回调又无法掌握每个要求的缓存情形。
当然如果沿着这个思路可以再扩展一个DataTaskDelegate工具以暴漏缓存入口,但是这么一来必须实现URLSessionDataDelegate,而且要想办法Swizzle NSURLSession的缓存代理(或者继续SessionDelegate切换代理),在代理中根据不同的NSURLDataTask进行缓存处理,全体过程对付调用方并不是太友好。

另一个思路便是等Response要求结束后获取缓存的相应CachedURLResponse并且修正(事实上只假如同一个NSURLRequest存储进去默认会更新原有缓存),而且NSURLCache本身便是有内存缓存的,过程并不会太耗时。
这个方案最主要的是得担保相应已经处理完成,以是这里通过Alamofire链式调用利用response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler重新要求以担保及时节制回调机遇。
紧张的代码片段如下:

要完玉成部缓存处理自然还包括缓存刷新、缓存清理等操作,关于缓存清理本身NSURLCache是供应了remove方法的,不过缓存清理的调用并不会立即生效,详细拜会NSURLCache does not clear stored responses in iOS8。
因此,这里借助了上面提到的Cache-Control进行缓存过期掌握,一方面可以快速清理缓存,另一方面缓存掌握可以更加精准。

AlamofireURLCache

为了更好的合营Alamofire利用,此代码以AlamofireURLCache类库形式放到了github上,所有接口API只管即便和原有接口保持同等,便于对Alamofire二次封装。
此外代码中还供应了手动清理缓存、出错之后自动清理缓存、覆盖做事器端缓存配置等功能。

AlamofireURLCache在request方法添加了refreshCache参数用于缓存刷新,设为false或者不供应此参数则不会刷新缓存,只有等到上次缓存数据过了有效期才会再次发起要求。

做事器端缓存headers设置并不都是最优选择,某些情形下客户端必须自行掌握缓存策略,可以利用AlamofireURLCache的ignoreServer参数忽略做事器端配置,通过maxAge参数自行掌握缓存时长。

其余,有些情形下未必需要刷新缓存而是要清空缓存担保下次访问时再利用最新数据,可以利用AlamofireURLCache供应的缓存清理API来完成。
不过对付要求出错、序列化出错等情形如果调用了cache(maxAge)方法进行缓存后,那么下次要求会利用缺点的缓存数据,须要开拓者根据返回情形自行调用API清理缓存,因此在AlamofireURLCache中供应了autoClearCache参数来自动处理这种情形。