最近公司项目中,之前做的上传下载列表被用户吐槽,不能后台下载,不能锁屏下载。于是就开始寻找解决办法。
因为在iOS7 就推出了NSURLSession ,我也知道它能够实现后台下载。(之前一个哥们在做某视频软件时的需求就是要求后台也可以下载)。
我就直接定位到了NSURLSession,开始着手重写app的上传下载模块。找了资料研究了一下NSURLSession。
资料:https://www.shinobicontrols.com/blog/ios7-day-by-day-day-1-nsurlsession
NSURLSession提供的功能:
1、通过URL将数据下载到内存
2、通过URL将数据下载到文件系统
3、将数据上传到指定URL
4、在后台完成上述功能
NSURLSession状态同时对应着多个连接,不像之前使用共享的一个全局状态。会话是通过工厂方法来创建配置对象。
总共有三种会话:
1. 默认的,进程内会话
2. 短暂的(内存),进程内会话
3. 后台会话
如果是简单的下载,我们只需要使用默认模式即可:
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
配置对象有很多属性。例如,可以设置TLS安全等级,TLS决定你可以使用cookies和超时时间。还有两个非常有趣的属性:allowsCellularAccess和discretionary。前一个属性表示当只有一个3G网络时,网络是否允许访问。设置 discretionary属性可以控制系统在一个合适的时机访问网络,比如有可用的WiFi,有充足的电量。这个属性主要针对后台回话的,所以在后台会话模式下默认是打开的。
当我们创建了一个会话配置对象后,就可以用它来创建会话对象了:
NSURLSession *inProcessSession; inProcessSession = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];注意:这里我们把自己设置为代理了。通过代理方法可以告诉我们数据传输进度以及获取认证信息。下面我们会实现一些合适的代理。
NSString *url = @"http://appropriate/url/here"; NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]]; NSURLSessionDownloadTask *cancellableTask = [inProcessSession downloadTaskWithRequest:request]; [cancellableTask resume];现在会话将会异步下载此url的文件内容。
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location { NSFileManager *fileManager = [NSFileManager defaultManager]; NSArray *URLs = [fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask]; NSURL *documentsDirectory = URLs[0]; NSURL *destinationPath = [documentsDirectory URLByAppendingPathComponent:[location lastPathComponent]]; NSError *error; [fileManager removeItemAtURL:destinationPath error:NULL]; BOOL success = [fileManager copyItemAtURL:location toURL:destinationPath error:&error]; if (success){ dispatch_async(dispatch_get_main_queue(), ^{ UIImage *image = [UIImage imageWithContentsOfFile:[destinationPath path]]; self.imageView.image = image; self.imageView.contentMode = UIViewContentModeScaleAspectFill; self.imageView.hidden = NO; }); } else { NSLog(@"Couldn't copy the downloaded file"); } if(downloadTask == cancellableTask) { cancellableTask = nil; } }这个方法在NSURLSessionDownloadTaskDelegate代理中。在代码中,我们获取到下载文件的临时目录,并把它保存到文档目录下(因为有个图片),然后显示给用户。
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error{ dispatch_async(dispatch_get_main_queue(), ^{ self.progressIndicator.hidden = YES; }); }如果error是nil,则证明下载是成功的,否则就要通过它来查询失败的原因。如果下载了一部分,这个error会包含一个NSData对象,如果后面要恢复任务可以用到。
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten BytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite { double currentProgress = totalBytesWritten / (double)totalBytesExpectedToWrite; dispatch_async(dispatch_get_main_queue(), ^{ self.progressIndicator.hidden = NO; self.progressIndicator.progress = currentProgress; }); }这是NSURLSessionDownloadTaskDelegate的另一个代理方法,我们用来计算进度并更新进度条。
- (IBAction)cancelCancellable:(id)sender { if(cancellableTask) { [cancellableTask cancel]; cancellableTask = nil; } }非常容易!当取消后,会回调这个URLSession:task:didCompleteWithError:代理方法,通知你去及时更新UI。当取消一个任务后,也十分可能会再一次回调这个代理方法 URLSession:downloadTask:didWriteData:BytesWritten:totalBytesExpectedToWrite: 。当然,didComplete 方法肯定是最后一个回调的。
- (IBAction)cancelCancellable:(id)sender { if(self.resumableTask) { [self.resumableTask cancelByProducingResumeData:^(NSData *resumeData) { partialDownload = resumeData; self.resumableTask = nil; }]; } }上面方法中,我们把待恢复的数据保存到一个变量中,方便后面恢复下载使用。
if(!self.resumableTask) { if(partialDownload) { self.resumableTask = [inProcessSession downloadTaskWithResumeData:partialDownload]; } else { NSString *url = @"http://url/for/image"; NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]]; self.resumableTask = [inProcessSession downloadTaskWithRequest:request]; } [self.resumableTask resume]; }如果我们有这个partialDownload这个数据对象,就可以用它来创建一个新的任务。如果没有,就按以前的步骤来创建任务。
- (NSURLSession *)backgroundSession { static NSURLSession *backgroundSession = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfiguration:@"com.shinobicontrols.BackgroundDownload.BackgroundSession"]; backgroundSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil]; }); return backgroundSession; }需要非常注意的是,通过给的后台token,我们只能创建一个后台会话,所以这里使用dispatch once block。token的目的是为了当应用重启后,我们可以通过它获取会话。创建一个后台会话,会启动一个后台传输守护进程,这个进程会管理数据并传输给我们。即使当应用挂起或者终止,它也会继续运行。
NSString *url = @"http://url/for/picture"; NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]]; self.backgroundTask = [self.backgroundSession downloadTaskWithRequest:request]; [self.backgrounTask resume];现在,即使你按home键离开应用,下载也会在后台继续(受开始提到的配置项控制)。
-(void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler { self.backgroundURLSessionCompletionHandler = completionHandler; }这里,我们获取内容通过completionHandler,当我们接收下载的数据并更新UI时会调用completionHandler。我们保存了 completionHandler(注意需要copy),让正在加载的View Controller来处理数据。当View Controller加载成功后,创建后台会话(并设置代理)。因此之前使用的相同代理方法就会被调用。
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location { // Save the file off as before, and set it as an image view//... if (session == self.backgroundSession) { self.backgroundTask = nil; SCAppDelegate *appDelegate = (SCAppDelegate *)[[UIApplication sharedApplication] delegate]; if(appDelegate.backgroundURLSessionCompletionHandler) { // Need to copy the completion handlervoid (^handler)() = appDelegate.backgroundURLSessionCompletionHandler; appDelegate.backgroundURLSessionCompletionHandler = nil; handler(); } } }需要注意的几个地方: