Kill应用以后断点续传的实现

以前使用NSURLSession作了一个断点续传的demo,主要实现了在下载的过程当中中断下载,而后能够再次启动延续上次的下载连接继续下载的功能.原理是将task的方法cancelByProducingResumeData的Block块中的resumeData获取下来,当再次下载的时候,经过session的downloadTaskWithResumeData方法使用该resumeData建立一个新的task,而后启动下载,就实现了断点续传的功能.可是若是说当前任务正在下载,程序切到后台以后被kill掉,当再次启动应用的时候,就没法继续上次的下载,也就是说,刚才的那种思路,只是适用于用户手动暂停在程序不退出的状况下实现的断点续传,若是应用直接终结则不会继续下载,也就是说并非真正意义上的断点续传,由于再次启动应用的时候,仍然要从新下载.git

因而我就在考虑,如何可以实现当应用意外退出的时候,再次启动应用,仍然能够继续上次的下载任务.咱们知道,resumenData中保存的数据是当前任务的下载信息,将其反序列化出来成为字符串的格式输出的话,其内容以下所示(部分冗余内容已经切除,重要信息已添加注释):github

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>NSURLSessionDownloadURL</key> <string>http://oarbi0614.bkt.clouddn.com/%E5%86%B0%E6%B2%B3%E4%B8%96%E7%BA%AA.mp4</string> 请求的地址 <key>NSURLSessionResumeBytesReceived</key> <integer>12038723</integer> 当前下载的文件大小 <key>NSURLSessionResumeCurrentRequest</key> <data> YnBsaXN0MDD..... </data> <key>NSURLSessionResumeEntityTag</key> <string>"ljECR82nRMhHvP8D5M9sGQuKBjgK"</string> <key>NSURLSessionResumeInfoTempFileName</key> <string>CFNetworkDownload_6XdKfZ.tmp</string> 下载使用的临时文件名 <key>NSURLSessionResumeInfoVersion</key> <integer>2</integer> <key>NSURLSessionResumeOriginalRequest</key> <data> YnBsaXN0MDD... </data> <key>NSURLSessionResumeServerDownloadDate</key> <string>Mon, 25 Jul 2016 10:42:23 GMT</string> </dict> </plist>

这个数据块里保存了当前下载的的状态,包括临时文件的名字以及当前下载的文件大小.当文件正在下载的时候,会在tmp文件夹内生成一个.tmp文件,保存了当前实际下载的数据,当经过session的downloadTaskWithResumeData方法使用该resumeData建立一个新的task,而后启动下载的时候,task会经过该数据块找到这个文件,而后继续下载.这样一来,貌似咱们只须要将resumeData数据块保存下来而且保存存储数据的.tmp文件就行了,当应用意外退出再次开启下载任务的时候,咱们只须要使用resumeData建立下载任务,而后将.tmp文件放在tmp文件夹下就行了.数据库

那么如今问题到了如何在程序意外退出的时候如何保存resumeData数据块和.tmp文件上了.咱们该如何实现呢?缓存

方法一:当程序意外退出的时候,当前的控制器会调用-(void)viewWillDisappear:(BOOL)animated,应用的代理会调用-applicationWillTerminate:(UIApplication *)application,咱们能够在这两个方法里作文章.session

在-(void)viewWillDisappear:(BOOL)animated或者-applicationWillTerminate:(UIApplication *)application方法里将resumeData保存到cache文件夹中 
要取出resuemData,必然是要经过并发

__weak typeof(self)weakSelf = self; [self.downloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) { //在这里能够获取到resumeData weakSelf.resumeData = resumeData; }];

这个方法来获取resumeData,可是在实际使用的时候,却遇到了这样的问题,不管是将该代码放在-(void)viewWillDisappear:(BOOL)animated仍是-applicationWillTerminate:(UIApplication *)application方法里,weakSelf.resumeData里始终是空的,通过试验,当程序运行的时候,该block块里的代码是不走的,可是在该Block块外的代码,所有都是执行的.咱们都知道,当建立session的时候,若是设置queue的时候传入参数为nil,那么他的代理方法都是在全局并发队列里完成的,难道是由于这个缘由么?是否是当程序意外退出的时候,这两个方法里的子线程代码都不会执行?我作了以下的实验:app

在这两个方法里添加以下代码
dispatch_async(dispatch_get_global_queue(0, 0), ^{ for(int i = 0; i < 100; i ++){ NSLog(@"%d",i); } });

当在后台kill掉应用的时候,有时候输出0 ,可是若是把下面的代码添加到该方法里,里面的代码仍然是不执行的async

dispatch_async(dispatch_get_main_queue(), ^{ for(int i = 0; i < 100; i ++){ NSLog(@"%d",i); } });

可是若是只把性能

for(int i = 0; i < 100; i ++){ NSLog(@"%d",i); }

这几行代码放在这两个方法中,则都是能够完整的执行的,使人百思不得其解.测试

可是思考一下能够发现: 
- 当咱们设置session为主线程的时候,通过检查能够发现cancelByProducingResumeData方法里的Block块是在主线程运行的; 
- 当咱们设置session线程为nil的时候,其代理方法是在全局并发队列里执行的,包括cancelByProducingResumeData方法里的Block块也是在子线程中运行的.

因此咱们能够推测一下,在OC底层,cancelByProducingResumeData方法颇有可能也是像dispatch_async方法同样是经过向线程队列里添加任务来得到执行机会的.加上上面的三个实验,我猜想是当程序终结的时候,只会执行主线程中的代码,此时若是再经过获取线程向主线程添加任务的话,那么该任务就不会添加到主线程队列里去,更别提往子线程里添加任务了.OC是编译型语言,当应用终结的时候,只有那些写到主线程上面的,编译好的代码在程序终结的时候能够获得执行,而在终结的时候动态添加的任务则没法添加成功,因此在这两个方法里,当程序意外终结的时候,是不可以获取到resumeData的.

方法二:动态保存

既然不能在程序终结的时候获取到resumeData,那么只能在下载的过程当中动态的保存了.当应用在下载的时候,会频繁的调用其代理方法:

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite{ } }

每当有数据过来的时候,都会调用这个方法,通过试验能够发现这个方法的调用频率是很高的,尤为是在全局并发队列里执行的时候,根据测试,该方法的调用频率达到了1200次以上,因此若是每次下载都进行保存的话,那么将会大大的影响应用的效率,由于文件流的输入输出自己就很占用性能,再加上如此高频率的调用,对性能的影响是不敢想像的.因此我作了这样的设计,每当下载文件的十分之一时,获取resumeData.

那么获取resumeData的问题解决了,接下来就是获取.tmp文件了.若是咱们要对.tmp文件进行操做,那么就必需要获取到该文件完整的文件名,由于tmp文件夹下绝大多数文件都是.tmp结尾的,并且每次下载产生的.tmp文件都是不一样的,因此若是将全部.tmp文件都保存的话,确定是很不合理的.这时候,就用到了咱们获取到的resumeData,由于通过反序列化咱们能够知道,resumeData中是保存着当前下载的临时文件名的,因此咱们能够对resumeData解析以后,取出其中的临时文件名,并且当下载的时候,其确定是放在tmp文件夹下的,有了这些东西,咱们就能够对.tmp文件进行保存了.

最终采用的方法流程:

由方法二咱们能够获取到resumeData和.tmp文件,有了这两个文件,咱们就能够在应用下次启动的时候继续上次未完成的下载,在文件每下载十分之一的时候保存一次.具体操做以下:

在session下载代理方法里检测文件下载过程,每当下载超过十分之一的时候,获取resumeData数据块和.tmp文件的路径,而后将resumeData写入到Cache文件夹下,将.tmp文件拷贝至Cache文件夹下,同时在Cache文件夹下创建一个plist文件,key值为.tmp文件的文件名,value值是一个Bool值,标记该文件是否下载完成,至关于该plist文件是管理下载文件的目录; 
当应用再次启动的时候,首先从Cache文件夹下读取下载目录,查找未下载完成的文件,找到后将以该文件命名的.tmp临时文件复制到tmp文件夹中,而后取出”Resume_” + 该文件名 命名的上次保存的resumeData数据,使用该数据建立task并开启下载.

备注:Caches文件夹是苹果为用户提供的缓存路径,应用重启该目录不会清考,tmp文件夹会清空,而Documents目录下的文件备份的时候被上传到iCloud而且很快就用完有限的空间,因此咱们选择在cache文件夹下缓存咱们须要的文件.

通过这样的处理,基本就实现了当程序意外退出的时候,再次启动仍然能够继续上次未完成的下载.不过对性能有些许影响并且不会百分之百续传上次未下载完的数据,会丢失一点点(由于并非即时保存的).若是使用FMDB或者其余的数据库来管理缓存的文件的话,效果会更好些,不过此次想本身动手写这些东西,就没有用.

这个功能我写了一个Demo,当下载好再次启动的时候,会直接播放本地文件,没有下载完会继续下载,若没有缓存数据则会从新下载.Demo里包括了我本身封装的一个基本的播放器和进度指示器.缓存的功能我写成了一个单例放在了工程里,方便给各位看官查看.这个Demo放在了GitHub上,地址为https://github.com/TheRuningAnt/KillAppDownload.git,欢迎下载查看.

若是各位有更好的思路或者建议的话,跪求指点,多谢!

相关文章
相关标签/搜索