https://m.oschina.net/blog/379516
NSURLConnection是2003年伴随着Safari一起发行的网络开发API,距今已经有十一年。当然,在这十一年间它表现的相当优秀,有大量的应用基础,这也是为什么前面花了那么长时间对它进行详细介绍的原因。但是这些年伴随着iPhone、iPad的发展,对于NSURLConnection设计理念也提出了新的挑战。在2013年WWDC上苹果揭开了NSURLSession的面纱,将它作为NSURLConnection的继任者。相比较NSURLConnection,NSURLSession提供了配置会话缓存、协议、cookie和证书能力,这使得网络架构和应用程序可以独立工作、互不干扰。另外,NSURLSession另一个重要的部分是会话任务,它负责加载数据,在客户端和服务器端进行文件的上传下载。
通过前面的介绍大家可以看到,NSURLConnection完成的三个主要任务:获取数据(通常是JSON、XML等)、文件上传、文件下载。其实在NSURLSession时代,他们分别由三个任务来完成:NSURLSessionData、NSURLSessionUploadTask、NSURLSessionDownloadTask,这三个类都是NSURLSessionTask这个抽象类的子类,相比直接使用NSURLConnection,NSURLSessionTask支持任务的暂停、取消和恢复,并且默认任务运行在其他非主线程中,具体关系图如下:
前面通过请求一个微博数据进行数据请求演示,现在通过NSURLSessionDataTask实现这个功能,其实现流程与使用NSURLConnection的静态方法类似,下面是主要代码:
-(void)loadJsonData{ //1.创建url NSString *urlStr=[NSString stringWithFormat:@"http://192.168.1.208/ViewStatus.aspx?userName=%@&password=%@",@"KenshinCui",@"123"]; urlStr =[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; NSURL *url=[NSURL URLWithString:urlStr]; //2.创建请求 NSURLRequest *request=[NSURLRequest requestWithURL:url]; //3.创建会话(这里使用了一个全局会话)并且启动任务 NSURLSession *session=[NSURLSession sharedSession]; //从会话创建任务 NSURLSessionDataTask *dataTask=[session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { if (!error) { NSString *dataStr=[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]; NSLog(@"%@",dataStr); }else{ NSLog(@"error is :%@",error.localizedDescription); } }]; [dataTask resume];//恢复线程,启动任务 }
下面看一下如何使用NSURLSessionUploadTask实现文件上传,这里贴出主要的几个方法:
#pragma mark 取得mime types -(NSString *)getMIMETypes:(NSString *)fileName{ return @"image/jpg"; } #pragma mark 取得数据体 -(NSData *)getHttpBody:(NSString *)fileName{ NSString *boundary=@"KenshinCui"; NSMutableData *dataM=[NSMutableData data]; NSString *strTop=[NSString stringWithFormat:@"--%@\nContent-Disposition: form-data; name=\"file1\"; filename=\"%@\"\nContent-Type: %@\n\n",boundary,fileName,[self getMIMETypes:fileName]]; NSString *strBottom=[NSString stringWithFormat:@"\n--%@--",boundary]; NSString *filePath=[[NSBundle mainBundle] pathForResource:fileName ofType:nil]; NSData *fileData=[NSData dataWithContentsOfFile:filePath]; [dataM appendData:[strTop dataUsingEncoding:NSUTF8StringEncoding]]; [dataM appendData:fileData]; [dataM appendData:[strBottom dataUsingEncoding:NSUTF8StringEncoding]]; return dataM; } #pragma mark 上传文件 -(void)uploadFile{ NSString *fileName=@"pic.jpg"; //1.创建url NSString *urlStr=@"http://192.168.1.208/FileUpload.aspx"; urlStr =[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; NSURL *url=[NSURL URLWithString:urlStr]; //2.创建请求 NSMutableURLRequest *request=[NSMutableURLRequest requestWithURL:url]; request.HTTPMethod=@"POST"; //3.构建数据 NSString *path=[[NSBundle mainBundle] pathForResource:fileName ofType:nil]; NSData *data=[self getHttpBody:fileName]; request.HTTPBody=data; [request setValue:[NSString stringWithFormat:@"%lu",(unsigned long)data.length] forHTTPHeaderField:@"Content-Length"]; [request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@",@"KenshinCui"] forHTTPHeaderField:@"Content-Type"]; //4.创建会话 NSURLSession *session=[NSURLSession sharedSession]; NSURLSessionUploadTask *uploadTask=[session uploadTaskWithRequest:request fromData:data completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { if (!error) { NSString *dataStr=[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]; NSLog(@"%@",dataStr); }else{ NSLog(@"error is :%@",error.localizedDescription); } }]; [uploadTask resume]; }
如果仅仅通过上面的方法或许文件上传还看不出和NSURLConnection之间的区别,因为拼接上传数据的过程和前面是一样的。事实上在NSURLSessionUploadTask中还提供了一个- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler方法用于文件上传。这个方法通常会配合“PUT”请求进行使用,由于PUT方法包含在Web DAV协议中,不同的WEB服务器其配置启用PUT的方法也不同,并且出于安全考虑,各类WEB服务器默认对PUT请求也是拒绝的,所以实际使用时还需做重分考虑,在这里不具体介绍,有兴趣的朋友可以自己试验一下。
使用NSURLSessionDownloadTask下载文件的过程与前面差不多,需要注意的是文件下载文件之后会自动保存到一个临时目录,需要开发人员自己将此文件重新放到其他指定的目录中。
-(void)downloadFile{ //1.创建url NSString *fileName=@"1.jpg"; NSString *urlStr=[NSString stringWithFormat: @"http://192.168.1.208/FileDownload.aspx?file=%@",fileName]; urlStr =[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; NSURL *url=[NSURL URLWithString:urlStr]; //2.创建请求 NSMutableURLRequest *request=[NSMutableURLRequest requestWithURL:url]; //3.创建会话(这里使用了一个全局会话)并且启动任务 NSURLSession *session=[NSURLSession sharedSession]; NSURLSessionDownloadTask *downloadTask=[session downloadTaskWithRequest:request completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) { if (!error) { //注意location是下载后的临时保存路径,需要将它移动到需要保存的位置 NSError *saveError; NSString *cachePath=[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; NSString *savePath=[cachePath stringByAppendingPathComponent:fileName]; NSLog(@"%@",savePath); NSURL *saveUrl=[NSURL fileURLWithPath:savePath]; [[NSFileManager defaultManager] copyItemAtURL:location toURL:saveUrl error:&saveError]; if (!saveError) { NSLog(@"save sucess."); }else{ NSLog(@"error is :%@",saveError.localizedDescription); } }else{ NSLog(@"error is :%@",error.localizedDescription); } }]; [downloadTask resume]; }
NSURLConnection通过全局状态来管理cookies、认证信息等公共资源,这样如果遇到两个连接需要使用不同的资源配置情况时就无法解决了,但是这个问题在NSURLSession中得到了解决。NSURLSession同时对应着多个连接,会话通过工厂方法来创建,同一个会话中使用相同的状态信息。NSURLSession支持进程三种会话:
下面将通过一个文件下载功能对两种会话进行演示,在这个过程中也会用到任务的代理方法对上传操作进行更加细致的控制。下面先看一下使用默认会话下载文件,代码中演示了如何通过NSURLSessionConfiguration进行会话配置,如果通过代理方法进行文件下载进度展示(类似于前面中使用NSURLConnection代理方法,其实下载并未分段,如果需要分段需要配合后台进行),同时在这个过程中可以准确控制任务的取消、挂起和恢复。
// // KCMainViewController.m // URLSession // // Created by Kenshin Cui on 14-03-23. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // #import "KCMainViewController.h" @interface KCMainViewController ()<NSURLSessionDownloadDelegate>{ UITextField *_textField; UIProgressView *_progressView; UILabel *_label; UIButton *_btnDownload; UIButton *_btnCancel; UIButton *_btnSuspend; UIButton *_btnResume; NSURLSessionDownloadTask *_downloadTask; } @end @implementation KCMainViewController #pragma mark - UI方法 - (void)viewDidLoad { [super viewDidLoad]; [self setUi]; } #pragma mark 界面布局 -(void)setUi{ //地址栏 _textField=[[UITextField alloc]initWithFrame:CGRectMake(10, 50, 300, 25)]; _textField.borderStyle=UITextBorderStyleRoundedRect; _textField.textColor=[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0]; _textField.text=@"[Objective-C.程序设计(第4版)].(斯蒂芬).林冀等.扫描版[电子书www.minxue.net].pdf"; [self.view addSubview:_textField]; //进度条 _progressView=[[UIProgressView alloc]initWithFrame:CGRectMake(10, 100, 300, 25)]; [self.view addSubview:_progressView]; //状态显示 _label=[[UILabel alloc]initWithFrame:CGRectMake(10, 130, 300, 25)]; _label.textColor=[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0]; [self.view addSubview:_label]; //下载按钮 _btnDownload=[[UIButton alloc]initWithFrame:CGRectMake(20, 500, 50, 25)]; [_btnDownload setTitle:@"下载" forState:UIControlStateNormal]; [_btnDownload setTitleColor:[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0] forState:UIControlStateNormal]; [_btnDownload addTarget:self action:@selector(downloadFile) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:_btnDownload]; //取消按钮 _btnCancel=[[UIButton alloc]initWithFrame:CGRectMake(100, 500, 50, 25)]; [_btnCancel setTitle:@"取消" forState:UIControlStateNormal]; [_btnCancel setTitleColor:[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0] forState:UIControlStateNormal]; [_btnCancel addTarget:self action:@selector(cancelDownload) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:_btnCancel]; //挂起按钮 _btnSuspend=[[UIButton alloc]initWithFrame:CGRectMake(180, 500, 50, 25)]; [_btnSuspend setTitle:@"挂起" forState:UIControlStateNormal]; [_btnSuspend setTitleColor:[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0] forState:UIControlStateNormal]; [_btnSuspend addTarget:self action:@selector(suspendDownload) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:_btnSuspend]; //恢复按钮 _btnResume=[[UIButton alloc]initWithFrame:CGRectMake(260, 500, 50, 25)]; [_btnResume setTitle:@"恢复" forState:UIControlStateNormal]; [_btnResume setTitleColor:[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0] forState:UIControlStateNormal]; [_btnResume addTarget:self action:@selector(resumeDownload) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:_btnResume]; } #pragma mark 设置界面状态 -(void)setUIStatus:(int64_t)totalBytesWritten expectedToWrite:(int64_t)totalBytesExpectedToWrite{ dispatch_async(dispatch_get_main_queue(), ^{ _progressView.progress=(float)totalBytesWritten/totalBytesExpectedToWrite; if (totalBytesWritten==totalBytesExpectedToWrite) { _label.text=@"下载完成"; [UIApplication sharedApplication].networkActivityIndicatorVisible=NO; _btnDownload.enabled=YES; }else{ _label.text=@"正在下载..."; [UIApplication sharedApplication].networkActivityIndicatorVisible=YES; } }); } #pragma mark 文件下载 -(void)downloadFile{ //1.创建url NSString *fileName=_textField.text; NSString *urlStr=[NSString stringWithFormat: @"http://192.168.1.208/FileDownload.aspx?file=%@",fileName]; urlStr =[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; NSURL *url=[NSURL URLWithString:urlStr]; //2.创建请求 NSMutableURLRequest *request=[NSMutableURLRequest requestWithURL:url]; //3.创建会话 //默认会话 NSURLSessionConfiguration *sessionConfig=[NSURLSessionConfiguration defaultSessionConfiguration]; sessionConfig.timeoutIntervalForRequest=5.0f;//请求超时时间 sessionConfig.allowsCellularAccess=true;//是否允许蜂窝网络下载(2G/3G/4G) //创建会话 NSURLSession *session=[NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];//指定配置和代理 _downloadTask=[session downloadTaskWithRequest:request]; [_downloadTask resume]; } #pragma mark 取消下载 -(void)cancelDownload{ [_downloadTask cancel]; } #pragma mark 挂起下载 -(void)suspendDownload{ [_downloadTask suspend]; } #pragma mark 恢复下载下载 -(void)resumeDownload{ [_downloadTask resume]; } #pragma mark - 下载任务代理 #pragma mark 下载中(会多次调用,可以记录下载进度) -(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite{ [self setUIStatus:totalBytesWritten expectedToWrite:totalBytesExpectedToWrite]; } #pragma mark 下载完成 -(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location{ NSError *error; NSString *cachePath=[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; NSString *savePath=[cachePath stringByAppendingPathComponent:_textField.text]; NSLog(@"%@",savePath); NSURL *saveUrl=[NSURL fileURLWithPath:savePath]; [[NSFileManager defaultManager] copyItemAtURL:location toURL:saveUrl error:&error]; if (error) { NSLog(@"Error is:%@",error.localizedDescription); } } #pragma mark 任务完成,不管是否下载成功 -(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error{ [self setUIStatus:0 expectedToWrite:0]; if (error) { NSLog(@"Error is:%@",error.localizedDescription); } } @end
演示效果:
NSURLSession支持程序的后台下载和上传,苹果官方将其称为进程之外的上传和下载,这些任务都是交给后台守护线程完成的,而非应用程序本身。即使文件在下载和上传过程中崩溃了也可以继续运行(注意如果用户强制退关闭应用程序,NSURLSession会断开连接)。下面看一下如何在后台进行文件下载,这在实际开发中往往很有效,例如在手机上缓存一个视频在没有网络的时候观看(为了简化程序这里不再演示任务的取消、挂起等操作)。下面对前面的程序稍作调整使程序能在后台完成下载操作:// // KCMainViewController.m // URLSession // // Created by Kenshin Cui on 14-03-23. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // #import "KCMainViewController.h" #import "AppDelegate.h" @interface KCMainViewController ()<NSURLSessionDownloadDelegate>{ NSURLSessionDownloadTask *_downloadTask; NSString *_fileName; } @end @implementation KCMainViewController #pragma mark - UI方法 - (void)viewDidLoad { [super viewDidLoad]; [self downloadFile]; } #pragma mark 取得一个后台会话(保证一个后台会话,这通常很有必要) -(NSURLSession *)backgroundSession{ static NSURLSession *session; static dispatch_once_t token; dispatch_once(&token, ^{ NSURLSessionConfiguration *sessionConfig=[NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"com.cmjstudio.URLSession"]; sessionConfig.timeoutIntervalForRequest=5.0f;//请求超时时间 sessionConfig.discretionary=YES;//系统自动选择最佳网络下载 sessionConfig.HTTPMaximumConnectionsPerHost=5;//限制每次最多一个连接 //创建会话 session=[NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];//指定配置和代理 }); return session; } #pragma mark 文件下载 -(void)downloadFile{ _fileName=@"1.mp4"; NSString *urlStr=[NSString stringWithFormat: @"http://192.168.1.208/FileDownload.aspx?file=%@",_fileName]; urlStr =[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; NSURL *url=[NSURL URLWithString:urlStr]; NSMutableURLRequest *request=[NSMutableURLRequest requestWithURL:url]; //后台会话 _downloadTask=[[self backgroundSession] downloadTaskWithRequest:request]; [_downloadTask resume]; } #pragma mark - 下载任务代理 #pragma mark 下载中(会多次调用,可以记录下载进度) -(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite{ // [NSThread sleepForTimeInterval:0.5]; // NSLog(@"%.2f",(double)totalBytesWritten/totalBytesExpectedToWrite); } #pragma mark 下载完成 -(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location{ NSError *error; NSString *cachePath=[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; NSString *savePath=[cachePath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@",[NSDate date]]]; NSLog(@"%@",savePath); NSURL *saveUrl=[NSURL fileURLWithPath:savePath]; [[NSFileManager defaultManager] copyItemAtURL:location toURL:saveUrl error:&error]; if (error) { NSLog(@"didFinishDownloadingToURL:Error is %@",error.localizedDescription); } } #pragma mark 任务完成,不管是否下载成功 -(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error{ if (error) { NSLog(@"DidCompleteWithError:Error is %@",error.localizedDescription); } } @end
当NSURLSession在后台开启几个任务之后,如果有其中几个任务完成后系统会调用此应用程序的-(void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler代理方法;此方法会包含一个competionHandler(此操作表示应用完成所有处理工作),通常我们会保存此对象;直到最后一个任务完成,此时会重新通过会话标识(上面sessionConfig中设置的)找到对应的会话并调用NSURLSession的-(void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session代理方法,在这个方法中通常可以进行UI更新,并调用completionHandler通知系统已经完成所有操作。具体两个方法代码示例如下:
-(void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler{ //backgroundSessionCompletionHandler是自定义的一个属性 self.backgroundSessionCompletionHandler=completionHandler; } -(void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session{ AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; //Other Operation.... if (appDelegate.backgroundSessionCompletionHandler) { void (^completionHandler)() = appDelegate.backgroundSessionCompletionHandler; appDelegate.backgroundSessionCompletionHandler = nil; completionHandler(); } }