上篇文章中,简单的写了NSURLSession的基本使用场景,这篇文章中,主要讲述下使用NSURLSession做断点下载,首先描述下做断点下载的各个不同场景:
在下载过程中可以对task(任务)做的操作为:suspend/cancel分别对应:暂停操作,取消操作,根据用户是否退出程序,在开始任务后,大致可以形成以下的不同场景为:
A:用户点击暂停,没有退出程序,此时点击恢复按钮,即可继续下载;
B:用户点击暂停,退出程序;
C:用户点击取消,没有退出程序,此时点击重新开始,可以继续下载;
D:用户点击取消,退出程序;
E:用户直接退出程序;
对于A和C的场景,由于都没有退出程序,使用以下的代码可以搞定;
#define CACHESPATH NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)
@interface ViewController ()<NSURLSessionDownloadDelegate>
@property(nonatomic,strong) NSURLSession* session;
@property(nonatomic,strong) NSURLSessionDownloadTask *task;
@property(nonatomic,strong) NSData* resumeData;
@end
@implementation ViewController
-(NSURLSession *)session
{
if (!_session) {
_session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[[NSOperationQueue alloc] init]];
}
return _session;
}
- (void)viewDidLoad {
[super viewDidLoad];
}
// 开始按钮,两种任务的方式,第一种是取消后重新开始,第二种是第一次开始任务
- (IBAction)beginBtn:(id)sender {
if (self.resumeData) {
self.task = [self.session downloadTaskWithResumeData:self.resumeData];
[self.task resume];
}else{
self.task = [self.session downloadTaskWithURL:[NSURL URLWithString:@"http://mp3.ffxia.com//13/%E4%BB%BB%E5%A6%99%E9%9F%B3-%E9%A3%8E%E7%AD%9D[68mtv.com].mp3"]];
[self.task resume];
}
}
// 暂停任务
- (IBAction)suspendBtn:(id)sender {
// 暂停
[self.task suspend];
}
- (IBAction)reusmeBtn:(id)sender {
// 暂停之后恢复下载
[self.task resume];
}
// 取消下载任务
- (IBAction)cancelBtn:(id)sender {
[self.task cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
self.resumeData = resumeData;
}];
}
#pragma mark -- NSURLSessionDownloadDelegate
-(void)URLSession:(NSURLSession *)session downloadTask:(nonnull NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(nonnull NSURL *)location
{
NSLog(@"%@",location);
NSLog(@"下载完毕,将loaction中的文件移到沙盒cache文件夹中");
}
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes
{
NSLog(@"下载继续");
}
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
NSLog(@"%f",1.0 * totalBytesWritten / totalBytesExpectedToWrite );
}
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
NSLog(@"下载结束");
}
对于B场景:由于使用downloadTask,下载的文件都在沙盒中储存临时文件的文件夹tmp内,则下次需要重新下载,这还是不太科学的;
对于D场景:调用self.task cancelByProducingResumeData,在回到的block中有resumeData文件,将该文件和tmp中的文件都移到沙盒的cache文件中,下次开始下载时进行判断,可以实现断点下载;
对于E场景:此时下载了一部分,但是直接退出应用程序后,就无法拿到已经下载的内容,与B场景相似,下次需要重新下载,不科学;
在这样的情况下,解决场景B和场景E断点下载的方法就转化为了得到程序退出时的resumeData,首先想到的解决办法就是在程序退出时,让B场景和E场景调用一下self.task cancelByProducingResumeData函数,从而产生resumeData进行保存,可以在applicationWillTerminate中发送通知UIApplicationWillTerminateNotification,但是此时只能保存主线程中的数据,而self.task cancelByProducingResumeData的block是在全局队列的子线程中进行,所以此时无法调用该函数的block,也就无法得到resumeData;
当时在想是否能动态的保存下载内容,即在函数
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
NSLog(@"%f",1.0 * totalBytesWritten / totalBytesExpectedToWrite );
}
中进行动态的保存,按比例保存下载的部分,也就是判断下载进入,当达到一定比例,调用下cancelByProducingResumeData,拿到resumeData,保存resumeData和tmp,此时对于所有的应用程序意外退出的场景,都可以实现断点续传:代码如下:
[self.task cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
self.resumeData = resumeData;
// NSLog(@"####%@",[[NSString alloc] initWithData:resumeData encoding:NSUTF8StringEncoding]);
// 对resumeData以dom的方式进行XML的解析
GDataXMLDocument * document=[[GDataXMLDocument alloc] initWithData:resumeData options:0 error:nil];
GDataXMLElement *rootElements = document.rootElement;
NSArray *elementsArray = [rootElements elementsForName:@"dict"];
BOOL isFileName = false;
NSString *fileName = nil;
for (GDataXMLElement *element in elementsArray) {
for (GDataXMLElement *sunElement in element.children) {
// 拿到需要保存的文件名.tmp
if (isFileName) {
fileName = sunElement.stringValue;
isFileName = false;
}
if ([sunElement.stringValue isEqualToString:@"NSURLSessionResumeInfoTempFileName"]) {
isFileName = true;
}
}
}
// 将文件名和resumeData组成一个NSDictionary的一个元素
if (fileName) {
// fileName = [fileName stringByDeletingPathExtension];
self.resumeDataDict = @{fileName:resumeData};
}
}];
//创建存储文件夹
NSString *resumeDataPath = [[CACHESPATH lastObject] stringByAppendingPathComponent:@"resumeData"];
if (![[NSFileManager defaultManager] fileExistsAtPath:resumeDataPath] ) {
[[NSFileManager defaultManager] createDirectoryAtPath:resumeDataPath withIntermediateDirectories:NO attributes:nil error:nil];
}
// 写入plist
[self.resumeDataDict writeToFile:[resumeDataPath stringByAppendingPathComponent:@"resumeDataDict.plist"] atomically:NO];
// 取出resumeDataDict中的key值
NSArray *allKeys = self.resumeDataDict.allKeys;
NSString *tmpPath = NSTemporaryDirectory();
//根据plist中的key值,取出对应的tmp值,移到cache中
for (NSString *key in allKeys) {
NSData *tmpData = [NSData dataWithContentsOfFile:[tmpPath stringByAppendingPathComponent:key]];
[tmpData writeToFile:[resumeDataPath stringByAppendingPathComponent:key] atomically:NO];
}
下载完成要清理无用的缓存,下次进入需进行判断是否已经下载;