说起下载第一个想起的就是ASI。一年前接手的新项目是核心功能是视频相关业务,在修改和解决视频下载相关的问题的时候让我体会到了ASI的下载的强大。后来新需求需要视频后台下载,使用NSURLSession的时候,更加深刻的体会到了ASI的强大好用。
后来替换下载的时候的原因:
- ASI开启后台下载功能,在iOS10的设备上,只能下载三分钟,然后就处于休眠状态
- AFN下载也是三分钟
- 测试后台下载的时候,不要用模拟器,使用用真机。模拟器APP处于后台时不会休眠。
NSURLSession的特点简介
通过NSURLSession创建的后台下载任务,保证了APP在后台或者退出的状态下,依然能进行下载任务,下载完成后通过唤醒APP,来将下载完成的数据保存到特定的位置。
- 在APP处于后台、锁屏状态下依然能后下载
- 最强大的是:APP在手动退出以及闪退后的状态下依然能够进行下载任务
NSURLSession
创建下载session
1 | NSString *bundleId = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleIdentifier"]; |
- 在创建下载session的时候,需要一个下载标识,该标识需要在整个个系统内保证唯一,所以使用APP的bundle id。
- sessionConfig.allowsCellularAccess 控制是否可以通过蜂窝网络下载
当APP手动退出或者闪退后,重新启动时获取正在下载的tasks
1 | NSMutableDictionary *dictM = [self.downloadSession valueForKey:@"tasks"]; |
AppDelegate后台下载回调
当APP处于后台下载状态时,需要处理下载完成后的数据的回调,这里就涉及了一个AppDelegate中的一个特别重要的回调
1 | -(void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)(void))completionHandler{ |
该代理方法使用场景分析(详细分析过程见下面):
- 不实现该代理方法,手动进入App调用相关代理方法,部分情况下异常
- 实现该方法,不执行completionHandler,某一下载任务完成后唤醒App,继续其它下载任务,异常
- 实现该方法,执行completionHandler,某一下载任务完成后唤醒App,继续其它下载任务,一切正常
相关操作
创建下载任务:其实通过session创建的任务是NSURLSessionDownloadTask的子类__NSCFBackgroundDownloadTask,是苹果的私有API。
1 | NSURL *downloadURL = [NSURL URLWithString:downloadURLString]; |
暂停下载:downloadTask有多种办法去暂停,但是我们选择有resume的下载方法,可以更加方便我们管理和多次暂停继续。
1 | [downloadTask cancelByProducingResumeData:^(NSData * resumeData) { |
继续下载:通过resumeData继续下载
1 | NSURLSessionDownloadTask *downloadTask = [self.downloadSession downloadTaskWithResumeData:data]; |
取消或者删除下载
1 | [downloadTask cancel]; |
相关代理方法说明
下载任务开始后,下载文件的进度回调方法。bytesWritten
某一断点续传过程中已经下载的数据大小, totalBytesWritten
已经下载的文件的大小;totalBytesExpectedToWrite
当前需要下载的文件的大小
1 | - (void)URLSession:(NSURLSession *)session |
下载任务继续开始下载时的回调方法
1 | - (void)URLSession:(NSURLSession *)session |
- 当资源发生重定向时回调的方法。NSURLSession内部自己处理定向回调
1 | - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task |
当下载任务完成后的代理回调方法,回调的参数location
是下载完成后的文件,在沙盒当中存在的路径
1 | - (void)URLSession:(NSURLSession *)session |
假如在后台下载完成的回调,会触发该回调方法。
1 | - (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session{ |
下载失败后的回调。暂停,停止,失败都会触发。区别是:正常状态下的暂停会回调resumeData,如果resumeData不为空的话我们需要保存该数据,方便下次继续。
1 | - (void)URLSession:(NSURLSession *)session |
后台下载相关回调
AppDelegate 回调方法3种情况分析:
实现代理 | 执行completionHandler | 现象 | |
---|---|---|---|
1 | 否 | 部分情况下异常 | |
2 | 是 | 否 | 部分情况下异常 |
3 | 是 | 是 | 一切正常 |
1. 不实现代理方法
当不实现AppDelegate代理方法的时候,简单的下载是没有任何问题。NSURLSession下载完成后相关代理方法执行顺序如图:
只有手动将后台的App进入前台后会调用成功的回调,处理相关的数据。假如有3个下载任务则会回调3次URLSession:downloadTask:didFinishDownloadingToURL:
方法。
缺点和问题:
- 如果下载完成后,不进入前台或者手动杀死进程,则丢失下载数据
- 等待下载的任务无法继续下载
- 下载完成后,在后台无法使用本地通知
2. 实现代理方法,不执行completionHandler
AppDelegate 方法-(void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)(void))completionHandler
completionHandler 作用:
首先看苹果SDK的解释:
// Applications using an NSURLSession with a background configuration may be launched or resumed in the background in order to handle the
// completion of tasks in that session, or to handle authentication. This method will be called with the identifier of the session needing
// attention. Once a session has been created from a configuration object with that identifier, the session’s delegate will begin receiving
// callbacks. If such a session has already been created (if the app is being resumed, for instance), then the delegate will start receiving
// callbacks without any action by the application. You should call the completionHandler as soon as you’re finished handling the callbacks.
最后两句是说明completionHandler的,大概意思是:回调callbacks,只要session创建将会开始接收到。回调callbacks没有对你的应用程序进行任何的处理,一旦完成处理callbacks,你应该调用completionHandler。
英语不好感觉说的云里雾里的。callbacks应该是暂停继续任务时受到的代理方法。说的是我们完成下载任务后应该调用completionHandler
completionHandler作用测试代理:
1 | #pragma mark - test code |
completionHandler具体作用:
这个回调的作用有点牛皮。通过上面代理可以测试出completionHandler,在第一个后台下载任务完成时回调,这时后台App已经被唤醒,定时器开始输出计时秒数。然后其它的下载任务完成时不会再次回调该方法。所有下载任务完成时,没有处理completionHandler计时器继续运行。 调用执行时completionHandler计时器停止运行,App继续处于休眠状态。
虽然不知道completionHandler做了哪些处理,但是通过测试现象得出大概的作用。他用来控制后台的App被唤醒后继续处于休眠状态,节约系统资源。
所以不执行completionHandler App如果不重新启动,处于后台时会一直在运行状态。下载任务正常。
3. 实现代理方法,执行completionHandler
上面我们分析了completionHandler大概作用。所以所有后台任务下载完成后调用completionHandler,是App处于正常的状态。相关代理调用顺序:
completionHandler调用时机:
所有的下载任务下载完成后调用。感兴趣的可以看YCDowloadSession)下载库对completionHandler的处理逻辑。
YCDownloadSession
YCDownloadSession是我写的一个视频后台下载的库。里面拥有我对视频下载的详细的处理过程和管理的库。
使用效果图
- 单文件下载测试
- 多视频下载测试
- 通知
GitHub连接
https://github.com/onezens/YCDownloadSession
欢迎各位关注该库,如果你有任何问题请issues我,将会随时更新新功能和解决存在的问题。
遇到的一些问题
这里总结下载开发YCDownloadSession下载库中碰到的一些问题
下载资源重定向的问题
YCDownloadSession 内部标识一下下载task的时候,使用的下载资源的URL来标识。如果该资源被301/302重定向到一个另一个URL后,会存在两个URL。标识用的URL在代理回调的NSURLSessionTask或者NSURLSessionDownloadTask的currentRequest中取的url,这样就出现了一个问题,重定向后通过URL拿不到下载的task;originalRequest属性可以拿到重定向前的URL使用该属性解决这个问题。苹果下载相关SDK关系图可看它们之间关系。断点续传
NSURLSessionDownloadTask的断点续传是由其内部自己控制实现。在暂停某一下载任务的时候有两个方法:cancel
内部自己控制断点续传数据,拿到对应task可以继续下载。如果拿不到,不可继续。cancelByProducingResumeData:^(NSData * resumeData) {}
通过session继续下载[downloadSession downloadTaskWithResumeData:data]
,需要自己保存处理resumeData,可以满足很多情况下的续传。
部分下载资源不可断点续传
YCDownloadSession Demo中的测试用的下载资源来自百度视频,可以正常下载。网易视频的资源和部分响应头不完整的资源在暂停下载之后拿不到resumeData而回调失败的情况。
百度视频资源:
通过Mac Terminal自带的curl命令获取响应头
1 | curl -I https://vd1.bdstatic.com/mda-hiqmm8s10vww26sx/mda-hiqmm8s10vww26sx.mp4\?playlist\=%5B%22hd%22%5D\&auth_key\=1506158514-0-0-6cde713ec6e6a15bd856fbb4f2564658\&bcevod_channel\=searchbox_feed |
1 | https://vd1.bdstatic.com/mda-hiqmm8s10vww26sx/mda-hiqmm8s10vww26sx.mp4\?playlist\=%5B%22hd%22%5D\&auth_key\=1506158514-0-0-6cde713ec6e6a15bd856fbb4f2564658\&bcevod_channel\=searchbox_feed |
响应头:
1 | HTTP/1.1 200 OK |
网易视频资源:
1 | http://flv2.bn.netease.com/videolib3/1706/07/gDNOH8458/HD/gDNOH8458-mobile.mp4 |
响应头:
1 | curl -I http://flv2.bn.netease.com/videolib3/1706/07/gDNOH8458/HD/gDNOH8458-mobile.mp4 |
响应头异常视频资源:
1 | https://www.zmzfile.com:9043/rt/route\?fileid\=152260954bdfa322725ba58df2ab1e2c2e3a6050 |
响应头:
1 | curl -I https://www.zmzfile.com:9043/rt/route\?fileid\=152260954bdfa322725ba58df2ab1e2c2e3a6050 |
百度视频是支持断点续传,网易视频和第三种不支持断点续传。不支持断点续传的原因是资源的响应头里面没有Accept-Ranges: bytes
和ETag
这个两个字段,所以在点击暂停的时候,苹果SDK回调失败。关于网络资源断点续传的介绍:http://blog.chinaunix.net/uid-24512513-id-3391252.html