NSURLSession学习
一、回望NSURLConnection
作为Core Foundation / CFNetwork 框架的APIs之上的一个抽象,NSURLConnection伴随着2003年Safari浏览器的原始发行版本而诞生。
NSURLConnection实际上指的是一组构成Foundation框架中URL加载系统的相互关联的组件:NSURLRequest,NSURLResponse,NSURLProtocol,NSURLCache,NSHTTPCookieStorage,NSURLCredentialStorage,以及和它同名NSURLConnection。
二、从NSURLConnection到NSURLSession
在WWDC 2013中,Apple的团队对NSURLConnection进行了重构,并推出了NSURLSession。在iOS9中,苹果已经废除了NSURLConnection的使用,使用NSURLSession代替。AFNetworking最新版也已经从依赖于NSURLConnection改为NSURLSession。
NSURLSession也是一组相互依赖的类,它的大部分组件和NSURLConnection中的组件相同如NSURLRequest,NSURLCache等。而NSURLSession的不同之处在于,它将NSURLConnection替换为NSURLSession和NSURLSessionConfiguration,以及3个NSURLSessionTask的子类:NSURLSessionDataTask, NSURLSessionUploadTask, 和NSURLSessionDownloadTask。
在程序在前台时,NSURLSession与NSURLConnection可以互为替代工作;程序在后台或者程序未运行时,NSURLSession可以在后台继续工作。注意,如果用户强制将程序关闭,NSURLSession会断掉。
三、使用方法:
1.创建一个NSURLSessionConfiguration,为创建NSURLSession设置工作模式和网络;
sessionConfiguration一旦创建,再修改对已经创建的session就不起作用。除非在URLRequest中改写了更严格的策略。
2.工作模式:
一般模式(default):工作模式类似于原来的NSURLConnection,可以使用缓存到硬盘的Cache,Cookie,鉴权。
及时模式(ephemeral):不使用缓存的Cache,Cookie,鉴权(session相关数据保存在内存中),适用于私密浏览和其它类似情况。
后台模式(background):在后台完成上传下载,创建Configuration对象的时候需要给一个NSString的ID用于追踪完成工作的Session是哪一个。
3.创建一个NSURLSession,方法有三种:
1 + (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration;
这种方法根据刚才创建的sessionConfiguration创建session,并默认创建一个新的queue处理session信息。
1 + (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration 2 delegate:(id<NSURLSessionDelegate>)delegate delegateQueue:(NSOperationQueue *)queue;
这种方法可以设置回调的delegate和queue,如果设置在mainQueue,就能在主线程进行回调。如果不指定线程,默认就是子线程。
注意:session对delegateQueue是强应用,如果不停止session或者退出应用,它会一直存在在内存中。
1 + (NSURLSession *)sharedSession;
这种方法创建的session,使用使用共享的会话,全局的Cache,Cookie和证书。但是:不能逐渐获取服务器数据、自定义连接行为受限、鉴权受限、app未运行情况下无法后台上传和下载。
4.创建一个NSURLRequest调用刚才的NSURLSession对象提供的Task函数,创建一个NSURLSessionTask。
根据职能不同Task有三种子类:
1)NSURLSessionUploadTask:上传用的Task,传完以后不会再下载返回结果;
通过request创建,在上传时指定文件源或数据源。
1 - (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL; 2 - (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromData:(NSData *)bodyData; 3 - (NSURLSessionUploadTask *)uploadTaskWithStreamedRequest:(NSURLRequest *)request;
在创建upload task对象时,通过completionHandler指定任务完成后的回调代码块:
1 - (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromData:(NSData *)bodyData completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler; 2 - (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler;
2)NSURLSessionDownloadTask:下载用的Task;
通过request对象或url创建:
1 - (NSURLSessionDownloadTask *)downloadTaskWithURL:(NSURL *)url;
2 - (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request;
通过request对象或url创建,同时指定任务完成后通过completionHandler指定回调的代码块:
1 - (NSURLSessionDownloadTask *)downloadTaskWithURL:(NSURL *)url completionHandler:(void (^)(NSURL *location, NSURLResponse *response, NSError *error))completionHandler; 2 - (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURL *location, NSURLResponse *response, NSError *error))completionHandler;
根据已经下载的数据,继续下载任务,也有直接创建和任务完成后通过completionHandler指定回调的代码块两种:
1 - (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData; 2 - (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData completionHandler:(void (^)(NSURL *location, NSURLResponse *response, NSError *error))completionHa
3)NSURLSessionDataTask:可以上传内容,上传完成后再进行下载。
通过request对象或url创建:
1 - (NSURLSessionDataTask *)dataTaskWithURL:(NSURL *)url; 2 - (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request;
通过request对象或url创建,同时指定任务完成后通过completionHandler指定回调的代码块:
1 - (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler; 2 - (NSURLSessionDataTask *)dataTaskWithURL:(NSURL *)url completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler;
4.NSURLSessionDelegate
1)session失败时会调用,如果使用- (void)invalidateAndCancel;方法会立即调用,并给出错误原因
1 - (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(NSError *)error;
2) 当遇到验证请求时会调用,completionHandler中应当包含如何处置验证请求和用于验证的证书
1 - (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler;
3)当后台session完成后会调用
1 - (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session;
5.NSURLSessionDelegate
1)当task以错误结束的时候调用
1 - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error;
2)task请求需要验证时调用
1 - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler;
3)task向服务器发送数据时调用
1 - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend;
4)task需要向远程服务器发送一个新的body stream时调用
1 - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task needNewBodyStream:(void (^)(NSInputStream *bodyStream))completionHandler;
5)task需要HTTP重定向时调用
1 - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest *))completionHandler
6.NSURLSessionDataDelegate
1)dataTask接收到响应时调用
1 - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler;
2)dataTask变成downloadTask时调用
1 - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask;
1)dataTask接收到数据时调用
1 - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
4)dataTask将缓存响应时调用
1 - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask willCacheResponse:(NSCachedURLResponse *)proposedResponse completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler;
7.NSURLSessionDownloadDelegate
1)downloadTask继续下载时调用,可以用来暂停和恢复下载
1 - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes;
2)downloadTask写入数据时调用,可以用来计算下载进度
1 - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite;
3)downloadTask结束下载时调用
1 - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location;
8.使用详解
1)如果是downloadTask创建的Session调用,Session与Delegate会在指定的OperationQueue中进行交互,交互过程的顺序图如下(假如不需要鉴权,即非HTTPS请求):
2)当不再需要连接时:
(1)调用Session的invalidateAndCancel直接关闭;
(2)或者调用finishTasksAndInvalidate等待当前Task全部结束后关闭;
这时Delegate会收到URLSession:didBecomeInvalidWithError:这个事件。Delegate收到这个事件之后会被解引用。
3)如果是一个BackgroundSession:在Task执行的时候,用户切到后台,Session会和ApplicationDelegate做交互。当程序切到后台后,在BackgroundSession中的Task还会继续下载,现在分三个场景分析下Session和Application的关系:
(1)当加入了多个Task,程序没有切换到后台。
这种情况Task会按照NSURLSessionConfiguration的设置正常下载,不会和ApplicationDelegate有交互。
(2)当加入了多个Task,程序切到后台,所有Task都完成下载。
在切到后台之后,Session的Delegate不会再收到Task相关的消息,直到所有Task全都完成后,系统会调用ApplicationDelegate的application:handleEventsForBackgroundURLSession:completionHandler:回调,之后“汇报”下载工作,对于每一个后台下载的Task调用Session的Delegate中的URLSession:downloadTask:didFinishDownloadingToURL:(成功的话)和URLSession:task:didCompleteWithError:(成功或者失败都会调用)
之后调用Session的Delegate回调URLSessionDidFinishEventsForBackgroundURLSession:。
注意:在ApplicationDelegate被唤醒后,会有个参数ComplietionHandler,这个参数是个Block,这个参数要在后面Session的Delegate中didFinish的时候调用一下,如下:
1 - (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler 2 { 3 /*Store the completion handler. The completion handler is invoked by the view controller's checkForAllDownloadsHavingCompleted method (if all the download tasks have been completed). */ 4 self.backgroundSessionCompletionHandler = completionHandler; 5 } 6 7 @end 8 9 //Session的Delegate 10 @implementation APLViewController 11 - (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session 12 { 13 APLAppDelegate *appDelegate = (APLAppDelegate *) 14 [[UIApplication sharedApplication] delegate]; 15 if (appDelegate.backgroundSessionCompletionHandler) { 16 void (^completionHandler() = appDelegate.backgroundSessionCompletionHandler; 17 appDelegate.backgroundSessionCompletionHandler = nil; 18 completionHandler(); 19 } 20 NSLog(@"All tasks are finished"); 21 } 22 @end
(3)当加入了多个Task,程序切到后台,下载完成了几个Task,然后用户又切换到前台。(程序没有退出)
切到后台之后,Session的Delegate仍然收不到消息。在下载完成几个Task之后再切换到前台,系统会先汇报已经下载完成的Task的情况,然后继续下载没有下载完成的Task,后面的过程同第一种情况。
(4)当加入了多个Task,程序切到后台,几个Task已经完成,但还有Task还没有下载完的时候强制退出程序,然后再进入程序的时候。(程序退出了)
最后这个情况比较有意思,由于程序已经退出了,后面没有下完Session就不在了后面的Task肯定是失败了。但是已经下载成功的那些Task,新启动的程序也没有听“汇报”的机会了。
经过实验发现,这个时候之前在NSURLSessionConfiguration设置的NSString类型的ID起作用了,当ID相同的时候,一旦生成Session对象并设置Delegate,马上可以收到上一次关闭程序之前没有汇报工作的Task的结束情况(成功或者失败)。但是当ID不相同,这些情况就收不到了,因此为了不让自己的消息被别的应用程序收到,或者收到别的应用程序的消息,ID还是和程序的Bundle名称绑定上比较好,至少保证唯一性。