一. 关于同一个url的多次请求
有时候,对同一个URL请求多次,返回的数据可能都是一样的,比如服务器上的某张图片,无论下载多少次,返回的数据都是一样的.
上面的情况会造成以下问题
(1) 用户流量的浪费
(2) 程序响应速度不够快,用户体验差
解决上面的问题,一般考虑对数据进行缓存.
二. 缓存的理解
为了提高程序的响应速度,可以考虑使用缓存(内存缓存\硬盘缓存)
第一次请求数据时,内存缓存中没有数据,硬盘缓存中没有数据,我们只能向服务器索取.
当服务器返回数据时,需要做以下步骤
(1) 使用服务器的数据(比如解析、显示)
(2) 将服务器的数据缓存到硬盘(沙盒)
此时缓存的情况是:内存缓存中有数据,硬盘缓存中有数据.
再次请求数据分为两种情况:
(1) 如果程序并没有被关闭, 一直在运行
那么此时内存缓存中有数据,硬盘缓存中有数据, 如果此时再次请求数据,直接使用内存缓存中的数据即可
(2) 如果程序重新启动
那么此时内存缓存已经消失,没有数据,硬盘缓存依旧存在,还有数据,如果此时再次请求数据,需要读取内存中缓存的数据.
注意:从硬盘缓存中读取数据后,内存缓存中又有数据了
三. 缓存---基础
(1) 简单说明
- 由于GET请求一般用来查询数据,POST请求一般是发大量数据给服务器处理(变动性比较大),因此一般只对GET请求进行缓存.
- 在ios中,可以使用 NSURLCache 类缓存数据.
- 在ios5以前,apple只支持内存缓存,在ios5的时候,允许磁盘缓存,NSURLCache 是根据NSURLRequest 来实现的,ios6以后,支持http和https(之前只支持http).
(2) NSURLCache
- ios中的缓存技术使用 NSURLCache 类
- 缓存原理:一个NSURLRequest对应一个NSCachedURLResponse
- 缓存技术:把缓存的数据都保存到数据库中
(3) NSURLCache的常见用法
//获得全局缓存对象(没必要手动创建)
+ (NSURLCache *)sharedURLCache;
//设置内存缓存的最大容量(字节为单位,默认为512KB)
- (void)setMemoryCapacity:(NSUInteger)memoryCapacity;
//设置硬盘缓存的最大容量(字节为单位,默认为10M)
- (void)setDiskCapacity:(NSUInteger)diskCapacity;
//设置自定义的NSURLCache作为应用缓存管理对象
+ (void)setSharedURLCache:(NSURLCache *)cache;
//设置内存、磁盘、磁盘路径
- (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);
硬盘缓存的位置:沙盒/Library/Caches
还有一些属性:
//内存缓存容量大小
@property NSUInteger memoryCapacity;
//磁盘缓存容量大小
@property NSUInteger diskCapacity;
//当前已用内存容量
@property (readonly) NSUInteger currentMemoryUsage;
//当前已用磁盘容量
@property (readonly) NSUInteger currentDiskUsage;
有兴趣的话你可以进入NSURLCache这个类里面看看.
(4) 缓存get请求
要想对某个GET请求进行数据缓存,非常简单
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
// 设置缓存策略
request.cachePolicy = NSURLRequestReturnCacheDataElseLoad;
只要设置了缓存策略,系统会自动利用NSURLCache进行数据缓存
(5) iOS对NSURLRequest提供了7种缓存策略:(实际上能用的只有4种)
NSURLRequestUseProtocolCachePolicy // 默认的缓存策略(取决于协议)
NSURLRequestReloadIgnoringLocalCacheData // 忽略缓存,重新请求
NSURLRequestReloadIgnoringLocalAndRemoteCacheData // 未实现
NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData // 忽略缓存,重新请求
NSURLRequestReturnCacheDataElseLoad// 有缓存就用缓存,没有缓存就重新请求
NSURLRequestReturnCacheDataDontLoad// 有缓存就用缓存,没有缓存就不发请求,当做请求出错处理(用于离线模式)
NSURLRequestReloadRevalidatingCacheData // 未实现
(6) 缓存的注意事项
缓存的设置需要根据具体的情况考虑,如果请求某个URL的返回数据:
- 经常更新:不能用缓存!比如股票、彩票数据
- 一成不变:果断用缓存
- 偶尔更新:可以定期更改缓存策略 或者 定期清除缓存
提示:如果大量使用缓存,会越积越大,建议定期清除缓存
四 你必须了解的
(1) 304状态码:
304是什么意思?为什么要返回304?什么时候返回304?
做网络缓存经常性的在返回304状态码情况下对数据进行处理
客户端发送了一个带条件的GET 请求且该请求已被允许,而文档的内容(自上次访问以来或者根据请求的条件)并没有改变,则服务器返回给客户端的不是请求的数据而是状态码304,意思是告诉你,数据没变化,你可以用之前缓存的数据来呈现界面.
例如:一些静态页面,如图片,html,js等,这些静态页面往往可能是服务器早已准备好的,用户访问时仅仅是下载而已. 那么针对这种静态页面,就可以仅仅通过304状态码来判断,内容是否发生了变化. (取自百度百科)
(2) Last-Modified、Etag、Expires、Cache-Control
你想实现网络缓存就需要遵循http的缓存协议,就需要在服务端与客户端进行相应的配置,以上字段你可以理解成是四种协议或者是四种实现缓存的必经方法(使用一种或两种)
1. Last-Modified
在客户端第一次请求某一个URL时,服务器端的返回状态会是200,内容是你请求的资源,同时返回给你的还有一个Last-Modified的属性(在 HttpReponse Header 中),这个属性来标记此文件在服务期端最后被修改的时间,格式类似这样:
Last-Modified:Tue, 24 Feb 2016 08:01:04 GMT
NSDictionary *dic = [NSDictionary new];
if ([task.response isKindOfClass:[NSHTTPURLResponse class]]) {
NSHTTPURLResponse *tempResponse = (NSHTTPURLResponse *)task.response;
dic = tempResponse.allHeaderFields;
NSLog(@"allHeaderFields = %@",dic);
}
客户端第二次请求此URL时,根据HTTP协议的规定,浏览器会向服务器传送If-Modified-Since报头(HttpRequest Header),询问该时间之后文件是否有被修改过,格式类似这样:
If-Modified-Since:Tue, 24 Feb 2016 08:01:04 GMT
[[APIClient sharedClient].requestSerializer setValue:@"Tue, 24 Feb 2016 08:01:04 GMT" forHTTPHeaderField:@"If-Modified-Since"];
如果服务器端的资源没有变化,则自动返回304状态码,内容为空,这样就节省了传输数据量,当服务器端代码发生改变或者重启服务器时,则重新发出资源,返回和第一次请求时类似,从而保证不向客户端重复发出资源,也保证当服务器有变化时,客户端能够得到最新的资源.
注:如果If-Modified-Since的时间比服务器当前时间(当前的请求时间request_time)还晚,会认为是个非法请求
很多时候可能不能仅仅通过标记时间来确定当前文件有没有更新,这个时候就需要用到 Etag
2 Etag
Etag 主要为了解决 Last-Modified 无法解决的一些问题。
1、 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET;
2、某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒)
3、某些服务器不能精确的得到文件的最后修改时间;
为此,HTTP/1.1 引入了 Etag(Entity Tags).Etag仅仅是一个和文件相关的标记,可以是一个版本标记,比如说v1.0.0或者说"2e681a-6-5d044840"这么一串看起来很神秘的编码。但是HTTP/1.1标准并没有规定Etag的内容是什么或者说要怎么实现,唯一规定的是Etag需要放在""内。
Etag的工作原理与Last-Modified类似,只是它俩标记文件的形式不同而已:Etag是以内容来计算一个标志,计算方式可以使用md5、SHA1等哈希算法,Last-Modified直接以时间作标记.
此处更新了Etag的工作原理(copy,之前写的没有层次感)
Etag - 工作原理
Etag由服务器端生成,客户端通过If-Match或者说If-None-Match这个条件判断请求来验证资源是否修改。常见的是使用If-None-Match.请求一个文件的流程可能如下:
====第一次请求===
1.客户端发起 HTTP GET 请求一个文件;
2.服务器处理请求,返回文件内容和一堆Header,Header中包括Etag(Etag:“5d8c72a5edda8d6a:3239″)(假设服务器支持Etag生成和已经开启了Etag).状态码200,(客户端此时应该把Etag值保存在本地)
网络图片:
====第二次请求===
1.客户端发起 HTTP GET 请求一个文件,注意这个时候客户端同时发送一个If-None-Match头,这个头的内容就是第一次请求时服务器返回的Etag:“5d8c72a5edda8d6a:3239″(如下所示)
[[APIClient sharedClient].requestSerializer setValue:@"5d8c72a5edda8d6a:3239" forHTTPHeaderField:@"If-None-Match"];
2.服务器判断发送过来的Etag和计算出来的Etag匹配,如果If-None-Match为Yes,说明文件内容更改了,返回200(当然还有新的包含在头文件中的Etag),客户端保存(更新)此Etag以便下次发送这个请求的时候使用,如果If-None-Match为False,返回304,客户端继续使用本地缓存;
流程很简单,问题是,如果服务器又设置了Cache-Control:max-age和Expires呢,怎么办?
答案是同时使用,也就是说在完全匹配If-Modified-Since和If-None-Match即检查完修改时间和Etag之后,服务器才能返回304.
3 Expires
给出的日期/时间之后的时间,就被响应认为是过时。如Expires:Thu, 02 Apr 2009 05:14:08 GMT
需和Last-Modified结合使用。用于控制请求文件的有效时间,当请求数据在有效期内时客户端浏览器从缓存请求数据而不是服务器端.当缓存中数据失效或过期,才决定从服务器更新数据。
4 Cache-Control
(1) Cache-control: max-age=5
表示当访问此网页后的5秒内再次访问不会去服务器 .
(2) Cache-Control: no-cache:这个很容易让人产生误解,使人误以为是响应不被缓存。实际上Cache-Control: no-cache是会被缓存的,只不过每次在向客户端(浏览器)提供响应数据时,缓存都要向服务器评估缓存响应的有效性.
(3) Cache-Control: no-store:这个才是响应不被缓存的意思.
下图是使用cache-control打印出来的 httpResponse.allHeaderFields 效果图
五 客户端使用NSURLCache配合AFNetworking进行网络缓存
1 为了更方便的使用 NSURLCache,你需要初始化并设置一个共享(全局)的URL缓存,在-application:didFinishLaunchingWithOptions:中完成设置
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSURLCache *URLCache = [[NSURLCache alloc]initWithMemoryCapacity:5 * 1024 *1024 diskCapacity:30 * 1024 *1024 diskPath:nil];
[NSURLCache setSharedURLCache:URLCache];
return YES;
}
这里我们设置了一个内存5M、磁盘30M的缓存!
缓存策略由请求(客户端)和回应(服务端)分别指定, 理解这些策略以及它们如何相互影响,是为您的应用程序找到最佳行为的关键.
2 我们使用Etag演示url缓存
NSURL *url = [NSURL URLWithString:@"http://www.baidu.com"];
NSMutableURLRequest *mutableRequest = [NSMutableURLRequest requestWithURL:url cachePolicy:(NSURLRequestReloadIgnoringCacheData) timeoutInterval:15.0];
//发送etag
NSString *etag = [[NSUserDefaults standardUserDefaults]objectForKey:@"url对应的key值"];
if (etag.length > 0) {
[mutableRequest setValue:etag forHTTPHeaderField:@"If-None-Match"];
}
[[NSURLSession sharedSession]dataTaskWithRequest:mutableRequest completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
NSLog(@"statusCode == %ld", httpResponse.statusCode);
if (httpResponse.statusCode == 304) {// 如果是304,使用本地缓存
// 根据请求获取到`被缓存的响应`!
NSCachedURLResponse *cacheResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:mutableRequest];
// 拿到缓存的数据,然后进行页面显示
id data = cacheResponse.data;
}else{//假设请求成功(200)
//获取并存储etag
NSString *etag = httpResponse.allHeaderFields[@"Etag"];
[[NSUserDefaults standardUserDefaults]setObject:etag forKey:@"url对应的key值"];
//显示数据
}
}];
Last-Modified与Etag基本上相同,这里就不做演示了,事实上我们在使用Last-Modified的时候多半都伴随着Expires的使用.
这里还有一个疑问,我们的数据缓存在哪里?
(App Sandbox)/Library/Caches/bundleId+项目名/下面有三个文件,以SQLite数据库文件的形式存放
3 定期处理缓存
// 定期处理缓存
if (缓存有效或者没到指定日期) {
request.cachePolicy = NSURLRequestReturnCacheDataElseLoad;
}
// 获得全局的缓存对象
NSURLCache *cache = [NSURLCache sharedURLCache];
if (缓存无效或者超时) {
//删除此request对应的缓存文件
[cache removeCachedResponseForRequest:request];
}
NSCachedURLResponse *response = [cache cachedResponseForRequest:request];
if (response) {
NSLog(@"---这个请求已经存在缓存");
} else {
NSLog(@"---这个请求没有缓存");
}