首先要说一件重要的事:
NSCache和NSURLCache一点关系也没有
NSCache和NSURLCache一点关系也没有
NSCache和NSURLCache一点关系也没有

需要注意的一点是:
设置NSURLCache的大小时,大多使用下面的代码

- (BOOL)application:(UIApplication *)applicationdidFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{NSURLCache *URLCache = [[NSURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024diskCapacity:20 * 1024 * 1024diskPath:nil];[NSURLCache setSharedURLCache:URLCache];
}

但是即使没有这两句代码,iOS也会自动参与缓存的,只不过使用的是系统创建的NSURLCache类,同样是可以通过NSURLCache的sharedURLCache方法获取。

在某些情况下,应用中的系统组件会将缓存的内存容量设为0MB,这就禁用了缓存。解决这个行为的一种方式就是通过自己的实现子类化NSURLCache,拒绝将内存缓存大小设为0。如可以使用如下代码进行设置:

@interface MKNonZeroingURLCache : NSURLCache@end@implementation MKNonZeroingURLCache- (void)setMemoryCapacity:(NSUInteger)memoryCapacity {if (memoryCapacity == 0) {return;}[super setMemoryCapacity:memoryCapacity];
}@end@implementation AppDelegate- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {MKNonZeroingURLCache *urlCache = [[MKNonZeroingURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024 diskCapacity:20 * 1024 * 1024 diskPath:nil];[NSURLCache setSharedURLCache:urlCache];return YES;
}
// ...
@end

另外,在应用没有运行的状态下,如系统遇到磁盘空间太小的情况,系统也会主动清除一些磁盘缓存的。

说点题外话:setSharedURLCache:这个方法的命名和编程行为也是可以学习的。它告诉我们,单例的创建并不都是一成不变的使用sharedXXX方法,也可以使用一个setSharedXXX:传递一个自定义的本类对象,虽然单例对象是外部创建而不是预设的,但是这样创建之后sharedXXX方法依然是获取单例的方法。

本篇文章主要介绍一种网络缓存优化的策略,实际上这个优化方案是提升网络性能的一个小方案。提升网络性能是一个大的课题,它主要包括以下几个方面的改善:

网络请求性能优化的策略
一.减少请求带宽
1.请求压缩
2.响应压缩
二.降低请求延迟
如:为NSURLReqeust开启管道支持
三.避免网络请求
主要是使用缓存优化

回到顶部

一种缓存优化方案

HTTP协议规格说明定义ETag为“被请求变量的实体值”。另一种说法是,ETag是一个可以与Web资源关联的记号(token)。Web资源可以是一个web页面、json或xml数据、文件等。Etag有点类似于文件hash或者说是信息摘要。

在浏览器默认的行为中,当进行一次URL请求,服务端会返回'Etag'响应头,下次浏览器请求相同的URL时,浏览器会自动将它设置为请求头'If-None-Match'的值。服务器收到这个请求之后,就开始做信息校验工作将自己本次产生的Etag与请求传递过来的'If-None-Match'对比,如果相同,则返回HTTP状态码304,并且response数据体中没有数据。

进一步剖析这个过程:第二次请求的时候从哪里获取到'Etag'的值并赋给请求头'If-None-Match'的?自然是浏览器的缓存中取出的。那么浏览器收到304状态码之后又干了什么?刚才说到response数据体中没有数据,但是浏览器仍需加载页面,它会从缓存中读取上次缓存的页面。

上面的浏览器和服务器的配合完成了这样一系列的工作:

if (本地没有缓存) {进行第一次请求
} else {本地有缓存取出上次response的Etag,作为这次请求的'If-None-Match'值进行网络请求if (服务器给的HTTP状态码 == 304) {// response的数据体为空,减少了一次数据传输// 缓存存在的先决条件满足,从缓存中取数据} else {// 不是304,说明请求的内容改变了,服务器给了新的数据,数据体不空// 使用最新的数据}
}

然而上面说的一大通都只是浏览器的行为,并不是iOS请求的默认行为,对于iOS开发而言,虽然不需要手动地管理缓存,但缓存策略会对上面的行为有影响。
iOS中定以的URLRequest缓存策略有以下几种:

typedef NS_ENUM(NSUInteger, NSURLRequestCachePolicy)
{NSURLRequestUseProtocolCachePolicy = 0,NSURLRequestReloadIgnoringLocalCacheData = 1, // 从不读取缓存,但请求后将response缓存起来NSURLRequestReloadIgnoringLocalAndRemoteCacheData = 4, // UnimplementedNSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData,// 以下两种在取缓存时,可能取到的是过期数据NSURLRequestReturnCacheDataElseLoad = 2, // 缓存中没有才去发起请求加载,有就不进行网络请求了NSURLRequestReturnCacheDataDontLoad = 3, // 缓存中没有不加载,绝不发起网络请求,缓存中没有则返回错误NSURLRequestReloadRevalidatingCacheData = 5, // Unimplemented
};

我们着重看一下默认缓存策略UseProtocolCachePolicy和忽略缓存的策略ReloadIgnoringLocalCacheData,

当使用默认的缓存策略时:

第一次请求一个URL时,会将response和数据缓存起来,
再次请求相同的URL时,会使用缓存中的Etag作为这次请求的request的'If-None-Match'值,这样服务端会返回304并且response的数据体为空,此时iOS会帮助读取缓存中的数据体,修改次请求的response,将HTTP状态码改为200,使用修改后的response和缓存中取到的data作为参数执行完成回调。

以上过程看起来似乎很完美,除了状态码不是304,其他的过程和浏览器几乎一致。但是他有一个缺陷,在研究这个缺陷之前我们先弄清一个这么一个事实:请求内容可以分为三种 1.脚本2.用数据渲染的页面3.静态文件。

对于脚本请求的处理,服务端是会忽略Etag,而每次都会处理,这样返回的数据都是新的,返回HTTP状态码为200.
对于用数据渲染的页面,服务器会按照一定的计算规则,计算渲染之后的Etag,然后对比,再决定返回的是304或者200.
对于静态文件,有些服务器具有检测静态文件改变的能力,一旦文件发生改变,服务器会立刻检测到,从而返回200给客户端,而有些服务器检测文件改变的功能是有延迟的,或者根本没有这种功能,这样即使文件的内容改变了,服务器仍然认为没有改变,于是对比Etag依然相等,结果返回304.(这次测试使用了apache和Express,默认配置下的apache对文件改变的检测是有延迟的,Express则是实时检测的)

根据以上的描述就会暴露出使用默认缓存策略的一点劣势,如果服务器不能实时检测文件改变状态,那么文件是否改变的比对结果是不准确的。最糟糕的情况就是:当文件改变了,服务器认为仍然没有改变,从而返回了304,而没有携带最新的数据。

ReloadIgnoringLocalCacheData策略时:

每次请求前都会忽略缓存,request的header从来不会附带'If-None-Match'值, 服务器每次处理成功后都是返回200,这样每次都会拿到服务器的数据(每次response的Date头都是新的值),服务器返回的response带有完整的数据体。iOS接收到数据之后,将response和数据缓存,并作为参数执行完成回调。

这里我们也能够看到使用ReloadIgnoringLocalCacheData策略暴漏出来的缺点:尽管服务器端的文件确实没有改变,但iOS依然不使用本地已有的缓存,而每次服务端还要将数据发给客户端,这样是多么浪费带宽!

用这个不好,用这个也不好,到底该如何

我们期望的状态是这样的:
对于服务端,无论怎么做的配置,都希望文件是否改变的检查结果是最准确的。对于iOS客户端,得到状态码200自然不要多做什么处理,如果得到状态码304,则从缓存中取到数据。

于是进行了如下的缓存优化方案:

- (void)refreshedRequest:(NSString *)urlString success:(void (^)(NSHTTPURLResponse *httpResponse, id responseData))successs failure:(void (^)(NSError *error))failure {NSMutableURLRequest *urlRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString]];NSURLCache *urlCache = [NSURLCache sharedURLCache];NSCachedURLResponse *cacheURLResponse = [urlCache cachedResponseForRequest:urlRequest];if (cacheURLResponse) {NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)cacheURLResponse.response;NSString *cachedResponseEtag = [httpResponse.allHeaderFields objectForKey:@"Etag"];if (cachedResponseEtag) {[urlRequest setValue:cachedResponseEtag forHTTPHeaderField:@"If-None-Match"];}}[urlRequest setCachePolicy:NSURLRequestReloadIgnoringCacheData];[[[NSURLSession sharedSession] dataTaskWithRequest:urlRequest completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {if (!error && successs) {NSHTTPURLResponse *newHttpResponse = (NSHTTPURLResponse *)response;if (newHttpResponse.statusCode == 304) {// cached in localsuccesss(newHttpResponse, cacheURLResponse.data);} else {// refreshed from serversuccesss(newHttpResponse, data);}} else {if (failure) {failure(error);}}}] resume];
}

这样每次请求都使用忽略缓存的策略,但是要附带着"If-None-Match"头,它的值是上次请求的响应头"Etag"的值,于是服务器会每次都实时检查文件的修改状态,得到一个准确的状态值,最后决定返回304还是200。若是200,iOS则直接使用新的response和新的数据;如果是304,则使用新的response和缓存中的data。
这样既能够获取到最新的数据有能够节约带宽。两全其美。

回到顶部

不可忽视的响应头'Last-Modified'和请求头'If-Modified-Since'

在上面说的服务端对文件的验证只涉及到ETag,而实际上服务端的验证过程比这个复杂,还需要使用'Last-Modified'值。'Last-Modified'值在服务器处理阶段代表着文件的上次修改时间,在处理结束后作为一个响应头放到response中。如果在请求中添加了'If-Modified-Since'头,并将这个值设置为上次请求时得到的响应头'Last-Modified'的值,那么这次请求,服务器的处理过程如下:

if 计算出的'ETag' != 请求头中的'If-Non-Match' || 查询到的'Last-Modified'(上次修改的时间) != 请求头中的'If-Modified-Since'返回的response状态码200 和 数据
else返回的reponse状态码304

'Etag'与'Last-Modified'不同的是:
'Etag'更强调的是实体内容,它代表着文件的信息摘要,它是由服务器计算出来的类似于md5的值,使用'Etag'的验证是基于内容的。
'Last-Modified'实际上就是文件上次修改的时间,仅仅是一个时间戳,是从文件属性读取出来的,使用'Last-Modified'的验证是基于时间的。

了解了这些我们就可以改造上面的代码,使用双重验证:

- (void)refreshedRequest:(NSString *)urlString success:(void (^)(NSHTTPURLResponse *httpResponse, id responseData))successs failure:(void (^)(NSError *error))failure {NSMutableURLRequest *urlRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString]];NSURLCache *urlCache = [NSURLCache sharedURLCache];NSCachedURLResponse *cacheURLResponse = [urlCache cachedResponseForRequest:urlRequest];if (cacheURLResponse) {NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)cacheURLResponse.response;NSString *cachedResponseEtag = [httpResponse.allHeaderFields objectForKey:@"Etag"];if (cachedResponseEtag) {[urlRequest setValue:cachedResponseEtag forHTTPHeaderField:@"If-None-Match"];}// 增加对上次修改时间的验证NSString *cachedResponseModified = [httpResponse.allHeaderFields objectForKey:@"Last-Modified"];if (cachedResponseModified) {[urlRequest setValue:cachedResponseModified forHTTPHeaderField:@"If-Modified-Since"];}}// ....
}

不过现在比较悲剧的是,各个web容器已经将Etag值的计算方法玩坏了,在计算Etag是依赖的参数不仅仅有文件的内容信息,还有文件的修改时间,这样一来,Etag的功能就相当于最初设计的Etag的功能+Last-Modified的功能。所以说上面的改造代码没有什么实在意义,只使用Etag就可以。

为了Etag确实不仅仅是基于内容的验证值,我做了一下测试:
先进行访问一次对http://127.0.0.1/blog文件(里面只有几个字符的文本文件)的访问,得到如下的response:

<NSHTTPURLResponse: 0x7fe143d170d0> { URL: http://127.0.0.1/blog } { status code: 304, headers {Connection = "Keep-Alive";Date = "Tue, 23 Feb 2016 04:16:36 GMT";Etag = "\"14-52c66cf22bd40\"";"Keep-Alive" = "timeout=5, max=100";Server = "Apache/2.4.16 (Unix) PHP/5.5.29";
} }
<7b0a0922 74657374 223a2268 656c6c6f 222c0a7d>

此时文件的MD5为:

MD5 (blog) = 35466082cffbc8fe4529a18a55f0260e

然后修改服务端文件的修改时间,但并没有修改文件的内容

# 将修改时间更改为2016年1月1日0点0分
touch -mt 201601010000 blog

这时文件的MD5值为:

MD5 (blog) = 35466082cffbc8fe4529a18a55f0260e # 没有改变

再次进行访问,这次访问使用忽略缓存的协议,并且带上Etag值,而不带修改时间值,得到的response是:

<NSHTTPURLResponse: 0x7fe143c0c870> { URL: http://127.0.0.1/blog } { status code: 200, headers {"Accept-Ranges" = bytes;Connection = "Keep-Alive";"Content-Length" = 20;Date = "Tue, 23 Feb 2016 04:20:42 GMT";Etag = "\"14-52833bf364000\"";"Keep-Alive" = "timeout=5, max=100";"Last-Modified" = "Thu, 31 Dec 2015 16:00:00 GMT";Server = "Apache/2.4.16 (Unix) PHP/5.5.29";
} }<7b0a0922 74657374 223a2268 656c6c6f 222c0a7d>

数据没有变,但是Etag仍然改变了。(以上是在apache+PHP的测试结果,使用Express也是这样)

回到顶部

'Keep-Alive'响应头和不离线的URLSession

"Keep-Alive"响应头会控制客户端进行发起请求的间隔。例如:

"Keep-Alive" = "timeout=5, max=100"

其中timeout值代表着最小间隔,也就是说如果这次发送请求之后,要在5秒之后发起的请求才会进行网络访问。
max值代表着最大的尝试次数,在timeout时间内发起请求会使这个值-1直到变为0再变为设定值。
以上两个值就控制着这样一个过程:刚刚访问的一个请求,获取到了数据并进行了缓存,如果还没有过去5秒再次发起同样的请求,则不进行网络访问,直接读取缓存并且将响应头修改为"Keep-Alive" = "timeout=5, max=99",如果这次请求还没过去5秒又进行请求,同样不进行网络访问,直接读取缓存,修改响应头为"Keep-Alive" = "timeout=5, max=98".....直到max变为0,再来一次又变回100.

看到这里我们又能体会到缓存优化的必要性,服务端当设定了这个响应头时,也可以不受影响地拿到实时数据。

这里还要说的一个问题是Session的在线状态,例如上面的访问中,每次访问需要使用相同的session才能做到max值不断-1,如果session值改变了,相当于一次新的请求,获得的始终是"Keep-Alive" = "timeout=5, max=100",也就是说,下面的两种状况是不同的。

- (void)onlyUseOneSession {NSMutableURLRequest *urlRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:kURLString]];[[[NSURLSession sharedSession] dataTaskWithRequest:urlRequest completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {// ...}] resume];
}- (void)useDifferentSession {NSMutableURLRequest *urlRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:kURLString]];NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];[[session dataTaskWithRequest:urlRequest completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {// ...}] resume];session = nil;
}

iOS的URLSession相当于一个浏览器窗口,它们虽然能够共用cookie和NSURLCache,但是每个session因配置不同和状态不同,会对相同url的访问有差别的。如果访问使用相同Session,那么就能公用一套配置和访问历史等信息,管理起来也是非常方便的,而如果使用的Session都是一些局部变量,那么使用之后就会离线,而且再也无法获取到这些session。因此在开发中建议使用[NSURLSession sharedSession];

回到顶部

'Expires'响应头

这个响应头的值也是一个时间,代表着连接过期时间,它允许客户端在这个时间之前不去发网络请求,与'Keep-Alive'功能相似,但是'Keep-Alive'指定的是时间长度,而'Expires'指定的是时刻。

当服务端返返回响应头有这个值,依然是使用优化过的缓存比较稳妥。

这篇文章的意义

有关针对'Etag'和HTTP304状态码进行优化缓存的文章不胜枚举,那么为什么还要这样一篇文章。我想原因主要有以下几个:
1.大家都知道这个缓存的原理,可是没有讲得太明白,或者干脆直接就不讲直接上代码。以至于有人存在这种想法:我每次都用默认缓存策略也好好的,你为什么要让我优化。我认为本篇文章对于默认缓存策略和忽略缓存策略二者的优缺点描述还是很有必要的。
2.很多人的代码并没有建立在使用系统的NSURLCache的基础上,更有甚者,直接使用自定义的属性存取'Etag'的值,apple看到这样的代码会哭的,那么本地缓存中信息存在的意义是什么。
3.我个人比较想让大家读一下NSCache和NSURLCache的那几篇文章,对平常的工作确实相当有帮助的。
4.虽然我不是mattt,但我觉得mattt从未讽刺过SDWebImage。请不要将缓存和持久化存储混为一谈,也不要将文件缓存和URL缓存混为一谈。

NSCache和NSURLCache网络缓存优化相关推荐

  1. iOS网络加载图片缓存策略之ASIDownloadCache缓存优化

    iOS网络加载图片缓存策略之ASIDownloadCache缓存优化 在我们实际工程中,很多情况需要从网络上加载图片,然后将图片在imageview中显示出来,但每次都要从网络上请求,会严重影响用户体 ...

  2. iOS网络缓存扫盲篇--使用两行代码就能完成80%的缓存需求

    原文地址:https://github.com/ChenYilong/ParseSourceCodeStudy/blob/master/02_Parse的网络缓存与离线存储/iOS网络缓存扫盲篇.md ...

  3. iOS网络缓存扫盲篇

    当我们在谈论缓存的时候,我们在谈论什么? GET网络请求缓存 80%的缓存需求:两行代码就可满足 控制缓存的有效性 文件缓存:借助ETag或Last-Modified判断文件缓存是否有效 Last-M ...

  4. iOS网络缓存扫盲篇 - 使用两行代码就能完成80%的缓存需求

    当我们在谈论缓存的时候,我们在谈论什么? GET网络请求缓存 80%的缓存需求:两行代码就可满足 控制缓存的有效性 文件缓存:借助ETag或Last-Modified判断文件缓存是否有效 Last-M ...

  5. 【Swift】 GETPOST请求 网络缓存的简单处理

    GET & POST 的对比 源码: https://github.com/SpongeBob-GitHub/Get-Post.git 1. URL - GET 所有的参数都包含在 URL 中 ...

  6. linux7内核优化,centos7 系统内核、网络等优化(适用高并发)

    centos7 系统内核.网络等优化(适用高并发) 发布时间:2020-9-22 9:57:13  浏览量:1707  [字体:大 中 小] 一.ssh连接优化 # 禁用dns解析 Port 5211 ...

  7. Linux内核网络性能优化

    Linux内核网络性能优化 1. 前言 2. Linux网络协议栈 3. DPDK 4. XDP 4.1 XDP主要的特性 4.2 XDP与DPDK的对比 4.3 应用场景 5. CPU负载均衡 5. ...

  8. 索引使用的限制条件,sql优化有哪些,数据同步问题(缓存和数据库),缓存优化

    索引使用的限制条件,sql优化有哪些,数据同步问题(缓存和数据库),缓存优化 索引使用的限制条件,sql优化有哪些 a,选取最适用的字段:在创建表的时候,为了获得更好的性能,我们可以将表中字段的宽度设 ...

  9. 使用NSURLCache 数据缓存

    iOS开发网络篇-数据缓存 一,关于同一个URL的多次请求 有时候,对同一个URL请求多次,返回的数据可能都是一样的,比如服务器上的某张图片,无论下载多少次,返回的数据都是一样的. (1)用户流量的浪 ...

最新文章

  1. JavaAgent 实现字节码注入
  2. 气门组的结构组成有哪些_你知道电线电缆是由哪些结构材料组成的吗?
  3. Linux学习-仅执行一次的工作排程
  4. CS224n学习笔记1-nlp介绍和词向量
  5. nginx+Tomcat实现动静分离架构
  6. nginx一个端口配置多域名服务
  7. 图解源码之java锁的获取和释放(AQS)篇
  8. 开课吧课堂之何时调用构造函数
  9. 关于 JVM 内存的 N 个问题(转)
  10. PJSIP视频通话客户端
  11. 谁将是互联网宝宝军团的最大劲敌?
  12. Oracle手动建库常见问题
  13. 从安装jdk开始(安装jdk的步骤)
  14. guid主分区表损坏如何处理_GUID格式GPT硬盘引导损坏了怎么修复
  15. 物联网应用技术和计算机应用技术哪个更好,物联网应用技术和计算机应用技术的优劣?...
  16. 建立自己的手写笔画图案
  17. 阿里微服务大牛奉命总结出500页Spring微服务架构笔记
  18. python批量下载ECMWF欧洲中心数据
  19. vlc在Ubuntu下的自动安装和手动安装
  20. Windows Server 2012/2016 在桌面上显示“我的电脑”图标

热门文章

  1. Docker的基本使用-Ubuntu18.04
  2. R语言笔记5:控制结构
  3. Error in apply(df$var1, 2, mean) : dim(X) must have a positive length
  4. R语言ggplot2可视化散点图并使用scale_y_log10函数配置Y轴对数坐标、使用ggforce包的facet_zoom函数将可视化结果中需要突出放大的区域进行放大(Zoom in)
  5. seaborn使用axes_dict函数获取displot函数生成的图像所有标题信息、使用set_title函数自定义设置多面板直方图标题(Multi-panel histogram‘s title)
  6. pandas删除数据行中的重复数据行、基于dataframe所有列删除重复行、基于特定数据列或者列的作何删除重复行、删除重复行并保留重复行中的最后一行、pandas删除所有重复行(不进行数据保留)
  7. R语言message函数、warning()函数和stop()函数输出程序运行健康状态信息实战
  8. 线性分类器与非线性分类器的区别是什么?有哪些优劣特性?
  9. 介绍一下K近邻(KNN)算法,KNeighbors和RadiusNeighbors的差异是什么?各有什么优势?
  10. python使用正则表达式寻找具有特定后缀的文件