上篇文章中,简单的写了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];
    }



下载完成要清理无用的缓存,下次进入需进行判断是否已经下载;