码迷,mamicode.com
首页 > 其他好文 > 详细

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

时间:2016-07-31 01:41:55      阅读:237      评论:0      收藏:0      [点我收藏+]

标签:

技术分享

之前使用NSURLSession做了一个断点续传的demo,主要实现了在下载的过程中中断下载,然后可以再次启动延续上次的下载链接继续下载的功能.原理是将task的方法cancelByProducingResumeData的Block块中的resumeData获取下来,当再次下载的时候,通过session的downloadTaskWithResumeData方法使用该resumeData创建一个新的task,然后启动下载,就实现了断点续传的功能.但是如果说当前任务正在下载,程序切到后台之后被kill掉,当再次启动应用的时候,就无法继续上次的下载,也就是说,刚才的那种思路,只是适用于用户手动暂停在程序不退出的情况下实现的断点续传,如果应用直接终结则不会继续下载,也就是说并不是真正意义上的断点续传,因为再次启动应用的时候,仍然要重新下载.

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

<?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,我们可以在这两个方法里做文章.

在-(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,那么他的代理方法都是在全局并发队列里完成的,难道是因为这个原因么?是不是当程序意外退出的时候,这两个方法里的子线程代码都不会执行?我做了如下的实验:

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

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

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,欢迎下载查看.

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

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

标签:

原文地址:http://www.cnblogs.com/LL--Blog/p/5722200.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!