NSURLSession的Download Task用于完成下载任务,本文介绍如何创建断点续传的下载任务和后台下载任务。
我们直接从分析Demo入手:
故事板如下:
只有一个View Controller,用于创建各种下载任务,并将下载后的图片显示到视图上,下载过程中会更新下载进度。
头文件代码如下:
#import <UIKit/UIKit.h> @interface ViewController : UIViewController <NSURLSessionDownloadDelegate> /* NSURLSessions */ @property (strong, nonatomic) NSURLSession *currentSession; // 当前会话 @property (strong, nonatomic, readonly) NSURLSession *backgroundSession; // 后台会话 /* 下载任务 */ @property (strong, nonatomic) NSURLSessionDownloadTask *cancellableTask; // 可取消的下载任务 @property (strong, nonatomic) NSURLSessionDownloadTask *resumableTask; // 可恢复的下载任务 @property (strong, nonatomic) NSURLSessionDownloadTask *backgroundTask; // 后台的下载任务 /* 用于可恢复的下载任务的数据 */ @property (strong, nonatomic) NSData *partialData; /* 显示已经下载的图片 */ @property (weak, nonatomic) IBOutlet UIImageView *downloadedImageView; /* 下载进度 */ @property (weak, nonatomic) IBOutlet UILabel *currentProgress_label; @property (weak, nonatomic) IBOutlet UIProgressView *downloadingProgressView; /* 工具栏上的按钮 */ @property (weak, nonatomic) IBOutlet UIBarButtonItem *cancellableDownload_barButtonItem; @property (weak, nonatomic) IBOutlet UIBarButtonItem *resumableDownload_barButtonItem; @property (weak, nonatomic) IBOutlet UIBarButtonItem *backgroundDownload_barButtonItem; @property (weak, nonatomic) IBOutlet UIBarButtonItem *cancelTask_barButtonItem; - (IBAction)cancellableDownload:(id)sender; // 创建可取消的下载任务 - (IBAction)resumableDownload:(id)sender; // 创建可恢复的下载任务 - (IBAction)backgroundDownload:(id)sender; // 创建后台下载任务 - (IBAction)cancelDownloadTask:(id)sender; // 取消所有下载任务 @end
一、创建普通的下载任务
这种下载任务是可以取消的,代码如下:
- (IBAction)cancellableDownload:(id)sender { if (!self.cancellableTask) { if (!self.currentSession) { [self createCurrentSession]; } NSString *imageURLStr = @"http://farm6.staticflickr.com/5505/9824098016_0e28a047c2_b_d.jpg"; NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:imageURLStr]]; self.cancellableTask = [self.currentSession downloadTaskWithRequest:request]; [self setDownloadButtonsWithEnabled:NO]; self.downloadedImageView.image = nil; [self.cancellableTask resume]; } }
如果当前的session为空,首先需要创建一个session(该session使用默认配置模式,其delegate为自己):
/* 创建当前的session */ - (void)createCurrentSession { NSURLSessionConfiguration *defaultConfig = [NSURLSessionConfiguration defaultSessionConfiguration]; self.currentSession = [NSURLSession sessionWithConfiguration:defaultConfig delegate:self delegateQueue:nil]; self.currentSession.sessionDescription = kCurrentSession; }
随后创建下载任务并启动。
这种任务是可取消的,即下次下载又从0.0%开始:
if (self.cancellableTask) { [self.cancellableTask cancel]; self.cancellableTask = nil; }
二、创建可恢复的下载任务
可恢复的下载任务支持断点续传,也就是如果暂停当前任务,在下次再执行任务时,将从之前的下载进度中继续进行。因此我们首先需要一个NSData对象来保存已经下载的数据:
/* 用于可恢复的下载任务的数据 */ @property (strong, nonatomic) NSData *partialData;
执行下载任务时,如果是恢复下载,那么就使用downloadTaskWithResumeData:方法根据partialData继续下载。代码如下:
- (IBAction)resumableDownload:(id)sender { if (!self.resumableTask) { if (!self.currentSession) { [self createCurrentSession]; } if (self.partialData) { // 如果是之前被暂停的任务,就从已经保存的数据恢复下载 self.resumableTask = [self.currentSession downloadTaskWithResumeData:self.partialData]; } else { // 否则创建下载任务 NSString *imageURLStr = @"http://farm3.staticflickr.com/2846/9823925914_78cd653ac9_b_d.jpg"; NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:imageURLStr]]; self.resumableTask = [self.currentSession downloadTaskWithRequest:request]; } [self setDownloadButtonsWithEnabled:NO]; self.downloadedImageView.image = nil; [self.resumableTask resume]; } }
在取消下载任务时,要将partialData数据保存起来,而且不要调用cancel方法:
else if (self.resumableTask) { [self.resumableTask cancelByProducingResumeData:^(NSData *resumeData) { // 如果是可恢复的下载任务,应该先将数据保存到partialData中,注意在这里不要调用cancel方法 self.partialData = resumeData; self.resumableTask = nil; }]; }
另外在恢复下载时,NSURLSessionDownloadDelegate中的以下方法将被调用:
/* 从fileOffset位移处恢复下载任务 */ - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes { NSLog(@"NSURLSessionDownloadDelegate: Resume download at %lld", fileOffset); }
三、创建后台下载任务
后台下载任务,顾名思义,当程序进入后台后,下载任务依然继续执行。
首先创建一个后台session单例,这里的Session配置使用后台配置模式,使用backgroundSessinConfiguration:方法配置时应该通过后面的参数为该后台进程指定一个标识符,在有多个后台下载任务时这个标识符就起作用了。
/* 创建一个后台session单例 */ - (NSURLSession *)backgroundSession { static NSURLSession *backgroundSess = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfiguration:kBackgroundSessionID]; backgroundSess = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil]; backgroundSess.sessionDescription = kBackgroundSession; }); return backgroundSess; }
在创建后台下载任务时,应该使用后台session创建,然后resume。
- (IBAction)backgroundDownload:(id)sender { NSString *imageURLStr = @"http://farm3.staticflickr.com/2831/9823890176_82b4165653_b_d.jpg"; NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:imageURLStr]]; self.backgroundTask = [self.backgroundSession downloadTaskWithRequest:request]; [self setDownloadButtonsWithEnabled:NO]; self.downloadedImageView.image = nil; [self.backgroundTask resume]; }
在程序进入后台后,如果下载任务完成,程序委托中的对应方法将被回调:
/* 后台下载任务完成后,程序被唤醒,该方法将被调用 */ - (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler { NSLog(@"Application Delegate: Background download task finished"); // 设置回调的完成代码块 self.backgroundURLSessionCompletionHandler = completionHandler; }
然后调用NSURLSessionDownloadDelegate中的方法:
以下是
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location中的方法,该方法只有下载成功才被调用:
else if (session == self.backgroundSession) { self.backgroundTask = nil; AppDelegate *appDelegate = [AppDelegate sharedDelegate]; if (appDelegate.backgroundURLSessionCompletionHandler) { // 执行回调代码块 void (^handler)() = appDelegate.backgroundURLSessionCompletionHandler; appDelegate.backgroundURLSessionCompletionHandler = nil; handler(); } }
另外无论下载成功与否,以下方法都会被调用:
/* 完成下载任务,无论下载成功还是失败都调用该方法 */ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { NSLog(@"NSURLSessionDownloadDelegate: Complete task"); dispatch_async(dispatch_get_main_queue(), ^{ [self setDownloadButtonsWithEnabled:YES]; }); if (error) { NSLog(@"下载失败:%@", error); [self setDownloadProgress:0.0]; self.downloadedImageView.image = nil; } }
取消后台下载任务时直接cancel即可:
else if (self.backgroundTask) { [self.backgroundTask cancel]; self.backgroundTask = nil; }
四、NSURLSessionDownloadDelegate
为了实现下载进度的显示,需要在委托中的以下方法中实现:
/* 执行下载任务时有数据写入 */ - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten // 每次写入的data字节数 totalBytesWritten:(int64_t)totalBytesWritten // 当前一共写入的data字节数 totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite // 期望收到的所有data字节数 { // 计算当前下载进度并更新视图 double downloadProgress = totalBytesWritten / (double)totalBytesExpectedToWrite; [self setDownloadProgress:downloadProgress]; } /* 根据下载进度更新视图 */ - (void)setDownloadProgress:(double)progress { NSString *progressStr = [NSString stringWithFormat:@"%.1f", progress * 100]; progressStr = [progressStr stringByAppendingString:@"%"]; dispatch_async(dispatch_get_main_queue(), ^{ self.downloadingProgressView.progress = progress; self.currentProgress_label.text = progressStr; }); }
从已经保存的数据中恢复下载任务的委托方法,fileOffset指定了恢复下载时的文件位移字节数:
/* Sent when a download has been resumed. If a download failed with an * error, the -userInfo dictionary of the error will contain an * NSURLSessionDownloadTaskResumeData key, whose value is the resume * data. */ - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes;
只有下载成功才调用的委托方法,在该方法中应该将下载成功后的文件移动到我们想要的目标路径:
/* Sent when a download task that has completed a download. The delegate should * copy or move the file at the given location to a new location as it will be * removed when the delegate message returns. URLSession:task:didCompleteWithError: will * still be called. */ - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location;
无论下载成功或失败都会调用的方法,类似于try-catch-finally中的finally语句块的执行。如果下载成功,那么error参数的值为nil,否则下载失败,可以通过该参数查看出错信息:
/* Sent as the last message related to a specific task. Error may be * nil, which implies that no error occurred and this task is complete. */ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error;
后台下载的运行结果:
启动任务后,进入后台:
下载完成后,控制台将会“通知”我们:
2014-02-05 18:30:39.767 DownloadTask[3472:70b] Application Delegate: App did become active 2014-02-05 18:30:43.734 DownloadTask[3472:70b] Application Delegate: App will resign active 2014-02-05 18:30:43.735 DownloadTask[3472:70b] Application Delegate: App did enter background 2014-02-05 18:30:45.282 DownloadTask[3472:70b] Application Delegate: Background download task finished 2014-02-05 18:30:45.285 DownloadTask[3472:4907] NSURLSessionDownloadDelegate: Finish downloading 2014-02-05 18:30:45.301 DownloadTask[3472:4907] NSURLSessionDownloadDelegate: Complete task
再次启动程序,可以看到加载好的页面:
可以看到,通过后台下载让我们的程序更加异步地运行。NSURLSession封装了对应的接口,让我们要执行的任务更加专门化,这个新的网络架构的功能真的很强大。
本文的Demo基于https://github.com/ShinobiControls/iOS7-day-by-day改写,内容基本一致。
原来的Demo也有一篇博客对应:iOS7 Day-by-Day :: Day 1 :: NSURLSession。
本文的Demo也已经上传,有兴趣的话可以下载看看。