从iOS7以来,苹果推出NSURLSession后,iOS现在可以实现真正的后台下载,这对我们iOSer来说是一个福音。
一个 NSURLSession
对象可以协调一个或多个 NSURLSessionTask
对象,并根据NSURLSessionTask
创建的 NSURLSessionConfiguration
实现不同的功能。使用相同的配置,你也可以创建多组具有相关任务的 NSURLSession
对象。要利用后台传输服务,你将会使用 [NSURLSessionConfiguration backgroundSessionConfiguration]
来创建一个会话配置。添加到后台会话的任务在外部进程运行,即使应用程序被挂起,崩溃,或者被杀死,它依然会运行。
下面我们来看看如何使用NSURLSession
下载用到的委托方法
- AppDelegate委托方法
//在应用处于后台,且后台任务下载完成时回调
- (void)application:(UIApplication *)application
handleEventsForBackgroundURLSession:(NSString *)identifier
completionHandler:(void (^)())completionHandler;
- NSURLSession委托方法
/* 在任务下载完成、下载失败
* 或者是应用被杀掉后,重新启动应用并创建相关identifier的Session时调用
*/
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error;
/* 应用在后台,而且后台所有下载任务完成后,
* 在所有其他NSURLSession和NSURLSessionDownloadTask委托方法执行完后回调,
* 可以在该方法中做下载数据管理和UI刷新
*/
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session;
注:最好将handleEventsForBackgroundURLSession
中completionHandler
保存,在该方法中待所有载数据管理和UI刷新做完后,再调用completionHandler()
- NSURLSessionDownloadTask委托方法
/* 下载过程中调用,用于跟踪下载进度
* bytesWritten为单次下载大小
* totalBytesWritten为当当前一共下载大小
* totalBytesExpectedToWrite为文件大小
*/
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite;
/* 下载恢复时调用
* 在使用downloadTaskWithResumeData:方法获取到对应NSURLSessionDownloadTask,
* 并该task调用resume的时候调用
*/
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didResumeAtOffset:(int64_t)fileOffset
expectedTotalBytes:(int64_t)expectedTotalBytes;
//下载完成时调用
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location;
注:在URLSession:downloadTask:didFinishDownloadingToURL
方法中,location只是一个磁盘上该文件的临时 URL,只是一个临时文件,需要自己使用NSFileManager将文件写到应用的目录下(一般来说这种可以重复获得的内容应该放到cache目录下),因为当你从这个委托方法返回时,该文件将从临时存储中删除。
创建后台下载的操作步骤
后台传输的的实现也十分简单,简单说分为三个步骤:
- 创建后台下载用的NSURLSession对象,设置为后台下载类型;
- 向这个对象中加入对应的传输的NSURLSessionTask,并开始下载;
- 在AppDelegate里实现
handleEventsForBackgroundURLSession
,以刷新UI及通知系统传输结束。 - 实现NSURLSessionDownloadDelegate中必要的代理
下面用代码来说明描述后台下载的流程
首先,我们看下后台下载的时序图

具体代码实现
- 创建一个后台下载对象
用dispatch_once创建一个用于后台下载对象,目的是为了保证identifier的唯一,文档不建议对于相同的标识符 (identifier) 创建多个会话对象。这里创建并配置了NSURLSession,将通过backgroundSessionConfiguration其指定为后台session并设定delegate。
- (NSURLSession *)backgroundURLSession {
static NSURLSession *session = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *identifier = @"com.yourcompany.appId.BackgroundSession";
NSURLSessionConfiguration* sessionConfig = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:identifier];
session = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:[NSOperationQueue mainQueue]];
});
return session;
}
- 向其中加入对应的传输用的NSURLSessionTask,并调用resume启动下载。
- (void)beginDownloadWithUrl:(NSString *)downloadURLString {
NSURL *downloadURL = [NSURL URLWithString:downloadURLString];
NSURLRequest *request = [NSURLRequest requestWithURL:downloadURL];
NSURLSession *session = [self backgroundURLSession];
NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithRequest:request];
[downloadTask resume];
}
- 在appDelegate中实现
handleEventsForBackgroundURLSession
,要注意的是,需要在handleEventsForBackgroundURLSession
中必须重新建立一个后台 session 的参照(可以用之前dispatch_once
创建的对象),否则NSURLSessionDownloadDelegate
和NSURLSessionDelegate
方法会因为没有 对 session 的 delegate 设置而不会被调用。
然后保存completionHandler()。
- (void)application:(UIApplication *)application
handleEventsForBackgroundURLSession:(NSString *)identifier
completionHandler:(void (^)())completionHandler {
NSURLSession *backgroundSession = [self backgroundURLSession];
NSLog(@"Rejoining session with identifier %@ %@", identifier, backgroundSession);
// 保存 completion handler 以在处理 session 事件后更新 UI
[self addCompletionHandler:completionHandler forSession:identifier];
}
- (void)addCompletionHandler:(CompletionHandlerType)handler
forSession:(NSString *)identifier {
if ([self.completionHandlerDictionary objectForKey:identifier]) {
NSLog(@"Error: Got multiple handlers for a single session identifier. This should not happen.\n");
}
[self.completionHandlerDictionary setObject:handler forKey:identifier];
}
注:handleEventsForBackgroundURLSession方法是在后台下载的所有任务完成后才会调用。如果当后台传输完成时,如果应用程序已经被杀掉,iOS将会在后台启动该应用程序,下载相关的委托方法会在 application:didFinishLaunchingWithOptions:
方法被调用之后被调用。
- 实现
URLSessionDidFinishEventsForBackgroundURLSession
,待所有数据处理完成,UI刷新之后在改方法中在调用之前保存的completionHandler()。
//NSURLSessionDelegate委托方法,会在NSURLSessionDownloadDelegate委托方法后执行
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
NSLog(@"Background URL session %@ finished events.\n", session);
if (session.configuration.identifier) {
// 调用在 -application:handleEventsForBackgroundURLSession: 中保存的 handler
[self callCompletionHandlerForSession:session.configuration.identifier];
}
}
- (void)callCompletionHandlerForSession:(NSString *)identifier {
CompletionHandlerType handler = [self.completionHandlerDictionary objectForKey: identifier];
if (handler) {
[self.completionHandlerDictionary removeObjectForKey: identifier];
NSLog(@"Calling completion handler for session %@", identifier);
handler();
}
}
- 在此,后台下载的基本功能已经具备了,如果还需要监听下载进度和对下载完成数据进行处理,则需要实现上面提到的委托方法
URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:
和URLSession:downloadTask:didFinishDownloadingToURL:
关于断点下载
对于断点下载需要考虑几个问题:
- 如何暂停下载,暂停后,如何继续下载?
- 下载失败后,如何恢复下载?
- 应用被用户杀掉后,如何恢复之前的下载?
针对这几个问题,我们一个来分析
- 如何暂停下载,暂停后,如何继续下载?
有两种方法 - 第一种,使用cancelByProducingResumeData
/* 对某一个NSURLSessionDownloadTask取消下载,取消后会回调给我们 resumeData,
* resumeData包含了下载任务的一些状态,之后可以用户恢复下载
*/
- (void)cancelByProducingResumeData:(void (^)(NSData * resumeData))completionHandler;
调用该方法会触发以下方法,会附带resumeData,用于恢复。
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error
对应恢复方法
//通过之前保存的resumeData,获取断点的NSURLSessionTask,调用resume恢复下载
NSURLSessionDownloadTask *task = [[self backgroundURLSession] downloadTaskWithResumeData:resumeData];
[task resume];
- 第二种,使用NSURLSessionDownloadTask的suspend方法
//暂停
[self.downloadTask suspend];
//恢复
[self.downloadTask resume];
通过以上的两个方法,就可以实现下载的暂停与恢复下载了
- 下载失败后,如何恢复下载?
下载失败后,可以通过以下代码来恢复下载
/* 该方法下载成功和失败都会回调,只是失败的是error是有值的,
* 在下载失败时,error的userinfo属性可以通过NSURLSessionDownloadTaskResumeData
* 这个key来取到resumeData(和上面的resumeData是一样的),再通过resumeData恢复下载
*/
- (void)URLSession:(NSURLSession *)sessiona
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error {
if (error) {
// check if resume data are available
if ([error.userInfo objectForKey:NSURLSessionDownloadTaskResumeData]) {
NSData *resumeData = [error.userInfo objectForKey:NSURLSessionDownloadTaskResumeData];
NSURLSessionTask *task = [[self backgroundURLSession] downloadTaskWithResumeData:resumeData];
[task resume];
}
}
}
- 应用被用户杀掉后,如何恢复之前的下载?
在应用被杀掉前,iOS系统保存应用下载sesson的信息,在重新启动应用,并且创建和之前相同identifier的session时(苹果通过identifier找到对应的session数据),iOS系统会对之前下载中的任务进行依次回调URLSession:task:didCompleteWithError:
方法,之后可以使用上面提到的下载失败时的处理方法进行恢复下载
知道这些后,看下前台下载的时序图对整个下载流程就了解了。

关于Session的生命周期,可以阅读 Apple 的 Life Cycle of a URL Session with Custom Delegates 文档,它讲解了所有类型的会话任务的完整生命周期。
后台下载的配置和限制
NSURLSessionConfiguration 允许你设置默认的HTTP头,配置缓存策略,限制使用蜂窝数据等等。其中一个选项是discretionary标志,这个标志允许系统为分配任务进行性能优化。这意味着只有当设备有足够电量时,设备才通过 Wifi 进行数据传输。如果电量低,或者只仅有一个蜂窝连接,传输任务是不会运行的。后台传输总是在 discretionary模式下运行。timeoutIntervalForResource属性,支持资源超时特性。你可以使用这个特性指定你允许完成一个传输所需的最长时间。内容只在有限的时间可用,或者在用户只有有限Wifi带宽的时间内无法下载或上传资源的情况下,你也可以使用这个特性。
最后,我们来说一说使用后台会话的几个限制。作为一个必须实现的委托,您不能对NSURLSession使用简单的基于 block的回调方法。后台启动应用程序,是相对耗费较多资源的,所以总是采用HTTP重定向。后台传输服务只支持HTTP和HTTPS,你不能使用自定义的协议。系统会根据可用的资源进行优化,在任何时候你都不能强制传输任务在后台进行。
另外,要注意的是在后台会话中,NSURLSessionDataTasks 是完全不支持的,你应该只出于短期的,小请求为目的使用这些任务,而不是用来下载或上传。其中发现一些需要注意的点,记录下来。
NSURLSession在iOS10上的Bug
在iOS10上,resumeData不能直接使用,会提示以下错误
*** -[NSKeyedUnarchiver initForReadingWithData:]: data is NULL
*** -[NSKeyedUnarchiver initForReadingWithData:]: data is NULL
Invalid resume data for background download. Background downloads must use http or https and must download to an accessible file.
解决方法有点小麻烦,具体解决方法可以参照以下Demo
关于在后台启动新的下载任务,苹果对这方面有限制
大致说明如下:
1.苹果的NSURLSession这个类会维护一个Delay值(即延时执行时间),用于后台启动任务延时执行时使用;
2.当在后台启动一个新任务时,苹果会对这个任务进行延时执行,延时时间苹果那边是有一个默认的延时时间,当后台启动的任务数越多,这个值就会成2的N-1
幂倍增长;
3.比如:假设苹果设定的延时时间为Delay
。当在后台启动了第一个任务时,这个任务的延时时间为Delay
,这个任务会在Delay
时间后开始执行;当启动在后台启动第二个任务时,这个任务的延时时间为2*Delay
,当启动第三个任务是,该任务的延时执行时间即为2*2*Delay
以此类推,在后台启动第N个任务是,该任务的延时执行时间为2(N-1)次方*Delay
;
4.但是在应用从后台切到前台或者重新启动时,这个延时时间会重置。
所以苹果对在后台启动新的下载或者上传任务时,是有限制的,苹果也是不建议这么处理的
以下为苹果官方说明的链接地址:
https://forums.developer.apple.com/thread/14854
网友评论
config.sessionSendsLaunchEvents = true
这样才能实现后台任务。
既然如此 是不是可以说 这个resulmData 在用户杀死app之前iOS已经给我保存了 我只需到时候拿来用即可???
看了很多博客 他们都是费劲巴列的在周期回调函数- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite这里面自己存储的resulmData感觉好麻烦啊!!!
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
这个方法里能否拿到本次写入的二进制数据 还是说只能拿到本次二进制数据写入的大小 不能拿到二进制数据本身
{
NSErrorFailingURLKey = "http://120.25.226.186:32812/resources/videos/minion_08.mp4";;
NSErrorFailingURLStringKey = "http://120.25.226.186:32812/resources/videos/minion_08.mp4";;
NSLocalizedDescription = cancelled;
}
该方法下载成功和失败都会回调,只是失败的是error是有值的,
* 在下载失败时,error的userinfo属性可以通过NSURLSessionDownloadTaskResumeData
* 这个key来取到resumeData(和上面的resumeData是一样的),再通过resumeData恢复下载
没这个resulmData值呢
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data;了,这个需要你自己拼接数据了。
UI 刷新是在这个方法执行?
为什么要调用completionHandler(),有什么作用.(文档上也是写的要调用,但也没说明原因)
我的现在是这个情况,因为我们做的是sdk所以,并没有AppDelegate.m,所以一直没有实现application: handleEventsForBackgroundURLSession: completionHandler:,但是之前的后台下载是一直好使的,不过不知道从什么时候开始,后台下载不行了,手机连着xcode的时候是可以正常后台的,但是手机直接启动,就会过段时间被系统自动杀死,求教,这个怎么解决下?
-(void)downloadSomeUrl
{
AppDelegate * delegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
NSArray * arr = @[@"http://dl1sw.baidu.com/soft/0a/14228/zhuanma_1.2.0.7.exe?version=2718134150",
@"http://dlsw.baidu.com/sw-search-sp/soft/20/36560/AshampooMP3CoverFinder_1.0.15.0.1430294531.exe",
@"http://dlsw.baidu.com/sw-search-sp/soft/90/16405/dvd2mp4_4.2.0.1.exe?version=425256212"];
delegate.urlArr = [NSMutableArray arrayWithArray:arr];
[delegate beginDownloadWithUrl:[delegate.urlArr firstObject]];
}
- (void)sendLocalNotification {
[self sendLocalNotificationWithTitle:@"单段下载完成"];
[_urlArr removeObjectAtIndex:0];
if (_urlArr.count > 0) {
[self beginDownloadWithUrl:[_urlArr firstObject]];
}else{
[self sendLocalNotificationWithTitle:@"全部下载完成"];
}
}
三个url的资源大小共计66M,但后台下载会比前台下载慢五分钟,我想问问您试过多个url在后台依次下载吗?
我也试过优酷下两集电视剧共500M,后台下载会比前台下载慢两分钟
不好意思,顺序下载这边是我没有仔细看文章,直接去看代码了!
1.
我还有一点不明白:
// 你必须重新建立一个后台 seesion 的参照
// 否则 NSURLSessionDownloadDelegate 和 NSURLSessionDelegate 方法会因为
// 没有 对 session 的 delegate 设定而不会被调用。参见上面的 backgroundURLSession
NSURLSession *backgroundSession = [self backgroundURLSession];
NSLog(@"Rejoining session with identifier %@ %@", identifier, backgroundSession);
你必须重新建立一个后台seesion的参照什么意思?
2.
NSURLSession * backgroundSession = [self backgroundURLSession];
这个backgroundSession和self.backgroundSession地址是一样的,不是说重新建立吗?我没明白这里的意图
3.
我把这两句
NSURLSession * backgroundSession = [self backgroundURLSession];
NSLog(@"Rejoining session with identifier %@ %@", identifier, backgroundSession);
注释了,一样下载,一点影响也没有
是不是可以这么理解,当进入后台下载的时候,程序并没有被真正杀死。直到所有下载完成,然后调用这个block,这个时候相当于告诉系统已经完事了,可以真正杀死了?
如果这样理解的话,URLSessionDidFinishEventsForBackgroundURLSession这个方法就是监控这个session里面所有的task的进度。但是又有个问题,如果我有两个session呢?是不是要在这个方法里面判断哪个session完成了,都完成才调用block?还有文章中<b>需要在handleEventsForBackgroundURLSession中必须重新建立一个后台 session 的参照(可以用之前dispatch_once创建的对象),否则 NSURLSessionDownloadDelegate 和 NSURLSessionDelegate 方法会因为没有 对 session 的 delegate 设置而不会被调用。</b>这个是怎么理解,没看明白。
以上链接是苹果的官方文档对backgroundSessionConfigurationWithIdentifier:的说明,上面说到
“Use this method to initialize a configuration object suitable for transferring data files while the app runs in the background. A session configured with this object hands control of the transfers over to the system, which handles the transfers in a separate process. In iOS, this configuration makes it possible for transfers to continue even when the app itself is suspended or terminated.
If an iOS app is terminated by the system and relaunched, the app can use the same identifier to create a new configuration object and session and retrieve the status of transfers that were in progress at the time of termination. ...”
就是程序不退出就不会被挂起
handleEventsForBackgroundURLSession:(NSString *)identifier
completionHandler:(void (^)())completionHandler
有没有人能告诉我 这个completionHandler里面到底是啥?为啥不马上执行,而要在URLSessionDidFinishEventsForBackgroundURLSession 这个回调里去执行呢?
但是依然显示
*** -[NSKeyedUnarchiver initForReadingWithData:]: data is NULL
Invalid resume data for background download. Background downloads must use http or https and must download to an accessible file
2016-09-26 09:46:17.928 BackgroundDownloadDemo[1076:31501] downloadTask:1 percent:5.09%
断网之后的error:
2016-09-26 09:46:18.095 BackgroundDownloadDemo[1076:31501] downloadTask:1 percent:5.26%
(lldb) po error
Error Domain=NSURLErrorDomain Code=-1002 "unsupported URL" UserInfo={NSLocalizedDescription=unsupported URL}
并没有收到resumeData,当再次将网络连接,没有继续下载。然后退出程序也没有走到error那个代理方法中,所以没有继续下载。
此时点击下载,是重新下载,而不是继续下载:
2016-09-26 09:49:18.977 BackgroundDownloadDemo[1138:34071] downloadTask:1 percent:0.20%
2016-09-26 09:49:19.083 BackgroundDownloadDemo[1138:34071] downloadTask:1 percent:0.41%
2016-09-26 09:49:19.186 BackgroundDownloadDemo[1138:34071] downloadTask:1 percent:0.63%
2016-09-26 09:49:19.341 BackgroundDownloadDemo[1138:34071] downloadTask:1 percent:0.78%
2016-09-26 09:49:19.495 BackgroundDownloadDemo[1138:34071] downloadTask:1 percent:1.00%
2016-09-26 09:49:19.599 BackgroundDownloadDemo[1138:34071] downloadTask:1 percent:1.22%
1.断网后,下载会停止,网络恢复时候,下载任务会自动恢复。你是在什么场景下进入下载失败回调,error里取不到resumedata?
2.退出应用,再次进入应用,重新初始化backgroundURLSession时,没有进入URLSession:task:didCompleteWithError:回调吗?这个时候应该可以从error中取到resumedata
1、你测试的时候电量一般多低就没法后台下载了?
2、有一些app都有一个选项,‘是否允许移动网络下载的’,这是不是意味着它并没有使用NSURLSession,而是用了其它的方法实现?
之前我回复你了。怎么被删了,那我在回复一次吧
1.低电量这个我也没测试过
2.我认为‘是否允许移动网络下载的’这个选择,只是针对应用在前台的时候才有效,在移动网络下,后台下载是被停止的。如果在移动网络下还是可以后台下载,那应该是通过其他途径让应用在后台不被苹果干掉,我知道是在后台循环播放无声的音乐这种方式,但是这个也是有风险的,可能会被苹果拒掉。