使用缓存的目的是为了使应用程序能更快速的响应用户输入,是程序高效的运行。有时候咱们须要将远程web服务器获取的数据缓存起来,以空间换取时间,减小对同一个url屡次请求,减轻服务器的压力,优化客户端网络,让用户体验更良好。git
背景:NSURLCache : 在iOS5之前,apple不支持磁盘缓存,在iOS5的时候,容许磁盘缓存,(NSURLCache 是根据NSURLRequest 来实现的)只支持http,在iOS6之后,支持http和https。github
缓存的实现说明:因为GET请求通常用来查询数据,POST请求通常是发大量数据给服务器处理(变更性比较大),所以通常只对GET请求进行缓存,而不对POST请求进行缓存。web
缓存原理:一个NSURLRequest对应一个NSCachedURLResponse数据库
缓存技术:把缓存的数据都保存到数据库中。json
NSURLCache的常见用法:api
(1)得到全局缓存对象(不必手动建立)NSURLCache *cache = [NSURLCache sharedURLCache]; 浏览器
(2)设置内存缓存的最大容量(字节为单位,默认为512KB)- (void)setMemoryCapacity:(NSUInteger)memoryCapacity;缓存
(3)设置硬盘缓存的最大容量(字节为单位,默认为10M)- (void)setDiskCapacity:(NSUInteger)diskCapacity;服务器
(4)硬盘缓存的位置:沙盒/Library/Caches网络
(5)取得某个请求的缓存- (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request;
(6)清除某个请求的缓存- (void)removeCachedResponseForRequest:(NSURLRequest *)request;
(7)清除全部的缓存- (void)removeAllCachedResponses;
缓存GET请求:
要想对某个GET请求进行数据缓存,很是简单
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
// 设置缓存策略
request.cachePolicy = NSURLRequestReturnCacheDataElseLoad;
只要设置了缓存策略,系统会自动利用NSURLCache进行数据缓存
iOS对NSURLRequest提供了7种缓存策略:(实际上能用的只有4种)
NSURLRequestUseProtocolCachePolicy // 默认的缓存策略(取决于协议)
NSURLRequestReloadIgnoringLocalCacheData // 忽略缓存,从新请求
NSURLRequestReloadIgnoringLocalAndRemoteCacheData // 未实现
NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData // 忽略缓存,从新请求
NSURLRequestReturnCacheDataElseLoad// 有缓存就用缓存,没有缓存就从新请求
NSURLRequestReturnCacheDataDontLoad// 有缓存就用缓存,没有缓存就不发请求,当作请求出错处理(用于离线模式)
NSURLRequestReloadRevalidatingCacheData // 未实现
缓存的注意事项:
缓存的设置须要根据具体的状况考虑,若是请求某个URL的返回数据:
(1)常常更新:不能用缓存!好比股票、彩票数据
(2)一成不变:果断用缓存
(3)偶尔更新:能够按期更改缓存策略 或者 清除缓存
提示:若是大量使用缓存,会越积越大,建议按期清除缓存
NSURLCache的属性介绍:
//获取当前应用的缓存管理对象 + (NSURLCache *)sharedURLCache; //设置自定义的NSURLCache做为应用缓存管理对象 + (void)setSharedURLCache:(NSURLCache *)cache; //初始化一个应用缓存对象 /* memoryCapacity 设置内存缓存容量 diskCapacity 设置磁盘缓存容量 path 磁盘缓存路径 内容缓存会在应用程序退出后 清空 磁盘缓存不会 */ - (instancetype)initWithMemoryCapacity:(NSUInteger)memoryCapacity diskCapacity:(NSUInteger)diskCapacity diskPath:(nullable NSString *)path; //获取某一请求的缓存 - (nullable NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request; //给请求设置指定的缓存 - (void)storeCachedResponse:(NSCachedURLResponse *)cachedResponse forRequest:(NSURLRequest *)request; //移除某个请求的缓存 - (void)removeCachedResponseForRequest:(NSURLRequest *)request; //移除全部缓存数据 - (void)removeAllCachedResponses; //移除某个时间起的缓存设置 - (void)removeCachedResponsesSinceDate:(NSDate *)date NS_AVAILABLE(10_10, 8_0); //内存缓存容量大小 @property NSUInteger memoryCapacity; //磁盘缓存容量大小 @property NSUInteger diskCapacity; //当前已用内存容量 @property (readonly) NSUInteger currentMemoryUsage; //当前已用磁盘容量 @property (readonly) NSUInteger currentDiskUsage;
与HTTP服务器进行交互的简单说明:
Cache-Control头
在第一次请求到服务器资源的时候,服务器须要使用Cache-Control这个响应头来指定缓存策略,它的格式以下:Cache-Control:max-age=xxxx,这个头指指明缓 存过时的时间
Cache-Control头具备以下选项:
public: 指示可被任何区缓存
private
no-cache: 指定该响应消息不能被缓存
no-store: 指定不该该缓存
max-age: 指定过时时间
min-fresh:
max-stable:
Last-Modified/If-Modified-Since
Last-Modified 是由服务器返回响应头,标识资源的最后修改时间.
If-Modified-Since 则由客户端发送,标识客户端所记录的,资源的最后修改时间。服务器接收到带有该请求头的请求时,会使用该时间与资源的最后修改时间进行对比,若是发现资源未被修改过,则直接返回HTTP 304而不返回包体,告诉客户端直接使用本地的缓存。不然响应完整的消息内容。
Etag/If-None-Match
Etag 由服务器发送,告之当资源在服务器上的一个惟一标识符。
客户端请求时,若是发现资源过时(使用Cache-Control的max-age),发现资源具备Etag声明,这时请求服务器时则带上If-None-Match头,服务器收到后则与资源的标识进行对比,决定返回200或者304。
服务器的文件存贮,大多采用资源变更后就从新生成一个连接的作法。并且若是你的文件存储采用的是第三方的服务,好比七牛、青云等服务,则必定是如此。
这种作法虽然是推荐作法,但同时也不排除不一样文件使用同一个连接。那么若是服务端的file更改了,本地已经有了缓存。如何更新缓存?
这种状况下须要借助 ETag
或 Last-Modified
判断图片缓存是否有效。
Last-Modified
顾名思义,是资源最后修改的时间戳,每每与缓存时间进行对比来判断缓存是否过时。
在浏览器第一次请求某一个URL时,服务器端的返回状态会是200,内容是你请求的资源,同时有一个Last-Modified的属性标记此文件在服务期端最后被修改的时间,格式相似这样:
Last-Modified: Fri, 12 May 2006 18:53:33 GMT
客户端第二次请求此URL时,根据 HTTP 协议的规定,浏览器会向服务器传送 If-Modified-Since 报头,询问该时间以后文件是否有被修改过:
If-Modified-Since: Fri, 12 May 2006 18:53:33 GMT
总结下来它的结构以下:
请求 HeaderValue | 响应 HeaderValue |
---|---|
Last-Modified | If-Modified-Since |
若是服务器端的资源没有变化,则自动返回 HTTP 304 (Not Changed.)状态码,内容为空,这样就节省了传输数据量。当服务器端代码发生改变或者重启服务器时,则从新发出资源,返回和第一次请求时相似。从而保证不向客户端重复发出资源,也保证当服务器有变化时,客户端可以获得最新的资源。
判断方法用伪代码表示:
if ETagFromServer != ETagOnClient || LastModifiedFromServer != LastModifiedOnClient GetFromServer else GetFromCache
之因此使用
LastModifiedFromServer != LastModifiedOnClient
而非使用:
LastModifiedFromServer > LastModifiedOnClient
缘由是考虑到可能出现相似下面的状况:服务端可能对资源文件,废除其新版,回滚启用旧版本,此时的状况是:
LastModifiedFromServer <= LastModifiedOnClient
但咱们依然要更新本地缓存。
实例:
/*! @brief 若是本地缓存资源为最新,则使用使用本地缓存。若是服务器已经更新或本地无缓存则从服务器请求资源。 @details 步骤: 1. 请求是可变的,缓存策略要每次都从服务器加载 2. 每次获得响应后,须要记录住 LastModified 3. 下次发送请求的同时,将LastModified一块儿发送给服务器(由服务器比较内容是否发生变化) @return 图片资源 */ - (void)getData:(GetDataCompletion)completion { NSURL *url = [NSURL URLWithString:kLastModifiedImageURL]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:15.0]; // // 发送 etag // if (self.etag.length > 0) { // [request setValue:self.etag forHTTPHeaderField:@"If-None-Match"]; // } // 发送 LastModified if (self.localLastModified.length > 0) { [request setValue:self.localLastModified forHTTPHeaderField:@"If-Modified-Since"]; } [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { // NSLog(@"%@ %tu", response, data.length); // 类型转换(若是将父类设置给子类,须要强制转换) NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; NSLog(@"statusCode == %@", @(httpResponse.statusCode)); // 判断响应的状态码是不是 304 Not Modified (更多状态码含义解释: https://github.com/ChenYilong/iOSDevelopmentTips) if (httpResponse.statusCode == 304) { NSLog(@"加载本地缓存图片"); // 若是是,使用本地缓存 // 根据请求获取到`被缓存的响应`! NSCachedURLResponse *cacheResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:request]; // 拿到缓存的数据 data = cacheResponse.data; } // 获取而且纪录 etag,区分大小写 // self.etag = httpResponse.allHeaderFields[@"Etag"]; // 获取而且纪录 LastModified self.localLastModified = httpResponse.allHeaderFields[@"Last-Modified"]; // NSLog(@"%@", self.etag); NSLog(@"%@", self.localLastModified); dispatch_async(dispatch_get_main_queue(), ^{ !completion ?: completion(data); }); }] resume]; }
ETag
是什么?
HTTP 协议规格说明定义ETag为“被请求变量的实体值” (参见 —— 章节 14.19)。 另外一种说法是,ETag是一个能够与Web资源关联的记号(token)。它是一个 hash 值,用做 Request 缓存请求头,每个资源文件都对应一个惟一的 ETag
值,
服务器单独负责判断记号是什么及其含义,并在HTTP响应头中将其传送到客户端,如下是服务器端返回的格式:
ETag: "50b1c1d4f775c61:df3" 客户端的查询更新格式是这样的: If-None-Match: W/"50b1c1d4f775c61:df3"
其中:
If-None-Match
- 与响应头的 Etag 相对应,能够判断本地缓存数据是否发生变化若是ETag没改变,则返回状态304而后不返回,这也和Last-Modified同样。
总结下来它的结构以下:
请求 HeaderValue | 响应 HeaderValue |
---|---|
ETag | If-None-Match |
ETag
是的功能与 Last-Modified
相似:服务端不会每次都会返回文件资源。客户端每次向服务端发送上次服务器返回的 ETag
值,服务器会根据客户端与服务端的 ETag
值是否相等,来决定是否返回 data,同时老是返回对应的 HTTP
状态码。客户端经过 HTTP
状态码来决定是否使用缓存。好比:服务端与客户端的 ETag
值相等,则 HTTP
状态码为 304,不返回 data。服务端文件一旦修改,服务端与客户端的 ETag
值不等,而且状态值会变为200,同时返回 data。
由于修改资源文件后该值会当即变动。这也决定了 ETag
在断点下载时很是有用。
好比 AFNetworking 在进行断点下载时,就是借助它来检验数据的。详见在 AFHTTPRequestOperation
类中的用法:
//下载暂停时提供断点续传功能,修改请求的HTTP头,记录当前下载的文件位置,下次能够从这个位置开始下载。 - (void)pause { unsigned long long offset = 0; if ([self.outputStream propertyForKey:NSStreamFileCurrentOffsetKey]) { offset = [[self.outputStream propertyForKey:NSStreamFileCurrentOffsetKey] unsignedLongLongValue]; } else { offset = [[self.outputStream propertyForKey:NSStreamDataWrittenToMemoryStreamKey] length]; } NSMutableURLRequest *mutableURLRequest = [self.request mutableCopy]; if ([self.response respondsToSelector:@selector(allHeaderFields)] && [[self.response allHeaderFields] valueForKey:@"ETag"]) { //若请求返回的头部有ETag,则续传时要带上这个ETag, //ETag用于放置文件的惟一标识,好比文件MD5值 //续传时带上ETag服务端能够校验相对上次请求,文件有没有变化, //如有变化则返回200,回应新文件的全数据,若无变化则返回206续传。 [mutableURLRequest setValue:[[self.response allHeaderFields] valueForKey:@"ETag"] forHTTPHeaderField:@"If-Range"]; } //给当前request加Range头部,下次请求带上头部,能够从offset位置继续下载 [mutableURLRequest setValue:[NSString stringWithFormat:@"bytes=%llu-", offset] forHTTPHeaderField:@"Range"]; self.request = mutableURLRequest; [super pause]; }
七牛等第三方文件存储商如今都已经支持ETag
,Demo8和9 中给出的演示图片就是使用的七牛的服务,见:
static NSString *const kETagImageURL = @"http://ac-g3rossf7.clouddn.com/xc8hxXBbXexA8LpZEHbPQVB.jpg";
下面使用一个 Demo 来进行演示用法,
以 NSURLConnection
搭配 ETag
为例,步骤以下:
NSURLRequestReloadIgnoringCacheData
,忽略本地缓存Etag
,服务器内容和本地缓存对比是否变化的重要依据If-None-Match
,而且传入 Etag
304
,说明本地缓存内容没有发生变化如下代码详见 Demo08 :
/*! @brief 若是本地缓存资源为最新,则使用使用本地缓存。若是服务器已经更新或本地无缓存则从服务器请求资源。 @details 步骤: 1. 请求是可变的,缓存策略要每次都从服务器加载 2. 每次获得响应后,须要记录住 etag 3. 下次发送请求的同时,将etag一块儿发送给服务器(由服务器比较内容是否发生变化) @return 图片资源 */ - (void)getData:(GetDataCompletion)completion { NSURL *url = [NSURL URLWithString:kETagImageURL]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:15.0]; // 发送 etag if (self.etag.length > 0) { [request setValue:self.etag forHTTPHeaderField:@"If-None-Match"]; } [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) { // NSLog(@"%@ %tu", response, data.length);dd // 类型转换(若是将父类设置给子类,须要强制转换) NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; NSLog(@"statusCode == %@", @(httpResponse.statusCode)); // 判断响应的状态码是不是 304 Not Modified (更多状态码含义解释: https://github.com/ChenYilong/iOSDevelopmentTips) if (httpResponse.statusCode == 304) { NSLog(@"加载本地缓存图片"); // 若是是,使用本地缓存 // 根据请求获取到`被缓存的响应`! NSCachedURLResponse *cacheResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:request]; // 拿到缓存的数据 data = cacheResponse.data; } // 获取而且纪录 etag,区分大小写 self.etag = httpResponse.allHeaderFields[@"Etag"]; NSLog(@"etag值%@", self.etag); !completion ?: completion(data); }]; }
相应的 NSURLSession
搭配 ETag 的版本见 Demo09:
/*! @brief 若是本地缓存资源为最新,则使用使用本地缓存。若是服务器已经更新或本地无缓存则从服务器请求资源。 @details 步骤: 1. 请求是可变的,缓存策略要每次都从服务器加载 2. 每次获得响应后,须要记录住 etag 3. 下次发送请求的同时,将etag一块儿发送给服务器(由服务器比较内容是否发生变化) @return 图片资源 */ - (void)getData:(GetDataCompletion)completion { NSURL *url = [NSURL URLWithString:kETagImageURL]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:15.0]; // 发送 etag if (self.etag.length > 0) { [request setValue:self.etag forHTTPHeaderField:@"If-None-Match"]; } [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { // NSLog(@"%@ %tu", response, data.length); // 类型转换(若是将父类设置给子类,须要强制转换) NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; NSLog(@"statusCode == %@", @(httpResponse.statusCode)); // 判断响应的状态码是不是 304 Not Modified (更多状态码含义解释: https://github.com/ChenYilong/iOSDevelopmentTips) if (httpResponse.statusCode == 304) { NSLog(@"加载本地缓存图片"); // 若是是,使用本地缓存 // 根据请求获取到`被缓存的响应`! NSCachedURLResponse *cacheResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:request]; // 拿到缓存的数据 data = cacheResponse.data; } // 获取而且纪录 etag,区分大小写 self.etag = httpResponse.allHeaderFields[@"Etag"]; NSLog(@"%@", self.etag); dispatch_async(dispatch_get_main_queue(), ^{ !completion ?: completion(data); }); }] resume]; }
Last-Modified
与 ETag
进行缓存以上的讨论是基于文件资源,那么对通常的网络请求是否也能应用?
控制缓存过时时间,无非两种:设置一个过时时间;校验缓存与服务端一致性,只在不一致时才更新。
通常状况下是不会对 api 层面作这种校验,只在有业务需求时才会考虑作,好比:
一些建议:
Last-Modified
就够了。即便 ETag
是首选,但此时二者效果一致。九成以上的需求,效果都一致。若是是通常的数据类型--基于查询的 get 请求,好比返回值是 data 或 string 类型的 json 返回值。那么 Last-Modified
服务端支持起来就会困难一点。由于好比
你作了一个博客浏览 app ,查询最近的10条博客, 基于此时的业务考虑 Last-Modified
指的是10条中任意一个博客的更改。那么服务端须要在你发出请求后,遍历下10条数据,获得“10条中是否至少一个被修改了”。并且要保证每一条博客表数据都有一个相似于记录 Last-Modified
的字段,这显然不太现实。
若是更新频率较高,好比最近微博列表、最近新闻列表,这些请求就不适合,更多的处理方式是添加一个接口,客户端将本地缓存的最后一条数据的的时间戳或 id 传给服务端,而后服务端会将新增的数据条数返回,没有新增则返回 nil 或 304。