【补充】NSURLSession 详解离线断点下载的实现

在上一篇文章开发只懂 AFN ?搞定 NSURLSession 才是硬道理中,我们已经对 NSURLSession 的基本使用有了简单认识,这里针对使用 NSURLSession 实现断点下载、离线断点下载等功能进行进一步拓展,希望看到这篇文章的朋友都能从中得到自己想要的知识。如有不足,欢迎指正!

NSURLSessionDataTask 大文件离线断点下载

  • 主要内容
    1. 实现文件下载
    2. 监听文件的下载进度
    3. 解决内存飙升问题
    4. 常用操作:开始 | 暂停 | 取消 | 恢复
    5. 断点下载
    6. 离线断点下载
    7. 实现源码

1. 实现文件下载

  • 对于文件下载的实现这里就不再赘述,如果记不太清的话可以参考篇头提到的文章,里面有详细介绍,这里我就上代码了
//01 确定请求路径
NSURL *URL = [NSURL URLWithString:@"http://sony.it168.com/data/attachment/forum/201410/20/2154195j037033ujs7cio0.jpg"];
//02 创建会话对象 设置代理
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]
                                delegate:self delegateQueue:[NSOperationQueue mainQueue]];
//03 创建请求 发送请求
[[session dataTaskWithURL:URL] resume];

2. 监听文件的下载进度

  • 利用代理来监听文件下载进度
  • 计算文件的下载进度 = 已经下载的 / 文件的总大小
-(void)URLSession:(NSURLSession *)session dataTask:(nonnull NSURLSessionDataTask *)dataTask 
didReceiveResponse:(nonnull NSURLResponse *)response 
completionHandler:(nonnull void (^)(NSURLSessionResponseDisposition))completionHandler {
        //子线程中执行
        NSLog(@"接收到服务器响应的时候调用 -- %@", [NSThread currentThread]);
    
        //得到请求文件的数据大小
        self.totalLength = response.expectedContentLength;
        //默认情况下不接收数据
        //必须告诉系统是否接收服务器返回的数据
        completionHandler(NSURLSessionResponseAllow);
}
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    
        NSLog(@"接受到服务器返回数据的时候调用,可能被调用多次");
        //拼接服务器返回的数据
        [self.fileData appendData:data];
        //计算文件的下载进度 = 已经下载的 / 文件的总大小
        self.progressView.progress = 1.0 * self.fileData.length / self.totalLength;
}
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    
        //保存数据 -> 沙盒
        NSString *fileName = task.response.suggestedFilename;
        NSString *cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
        NSString *fullPath = [cachePath stringByAppendingPathComponent:fileName];
    
        [self.fileData writeToFile:fullPath atomically:YES];
        self.fileData = nil;
}

做完以上两步之后就可以实现文件的下载操作和监听下载进度,但是此时会有很多问题,比如:内存飙升下载进度错乱无法控制下载状态等等,对于这些存在的问题,我们下面将一一进行解决。

初步实现效果(存在问题)

3. 解决内存飙升问题

  • 产生的原因:在下载文件的过程中,系统会先把文件保存在内存中,等到文件下载完毕之后再写入到磁盘
  • 解决方案:在下载文件时,一边下载一边写入到磁盘,减小内存使用
  • 在 iOS 中常用的有两种方法可以实现:
    • NSFileHandle 文件句柄
    • NSOutputStream 输出流
  • 方案一:NSFileHandle 文件句柄,大致分为四个步骤
    • 对代理方法进行改良
    -(void)URLSession:(NSURLSession *)session dataTask:(nonnull NSURLSessionDataTask *)dataTask 
didReceiveResponse:(nonnull NSURLResponse *)response 
completionHandler:(nonnull void (^)(NSURLSessionResponseDisposition))completionHandler {
            //接受到响应的时候 告诉系统如何处理服务器返回的数据
            completionHandler(NSURLSessionResponseAllow);
            //得到请求文件的数据大小
            self.totalLength = response.expectedContentLength;
            //拼接文件的全路径
            NSString *fileName = response.suggestedFilename;
            NSString *cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
            NSString *fullPath = [cachePath stringByAppendingPathComponent:fileName];
            
            //【1】在沙盒中创建一个空的文件
            [[NSFileManager defaultManager] createFileAtPath:fullPath contents:nil attributes:nil];
            //【2】创建一个文件句柄指针指向该文件(默认指向文件开头)
            self.handle = [NSFileHandle fileHandleForWritingAtPath:fullPath];
    }
    -(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
            //【3】使用文件句柄指针来写数据(边写边移动)
            [self.handle writeData:data];
            //累加已经下载的文件数据大小
            self.currentLength += data.length;
            //计算文件的下载进度 = 已经下载的 / 文件的总大小
            self.progressView.progress = 1.0 * self.currentLength / self.totalLength;
    }
    -(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
            //【4】关闭文件句柄
            [self.handle closeFile];
    }
  • 方案二:NSOutputStream 输出流,大致分为三个步骤
    • 对代理方法的处理
    -(void)URLSession:(NSURLSession *)session dataTask:(nonnull NSURLSessionDataTask *)dataTask 
didReceiveResponse:(nonnull NSURLResponse *)response 
completionHandler:(nonnull void (^)(NSURLSessionResponseDisposition))completionHandler {
            //接受到响应的时候 告诉系统如何处理服务器返回的数据
            completionHandler(NSURLSessionResponseAllow);
            //得到请求文件的数据大小
            self.totalLength = response.expectedContentLength;
            //拼接文件的全路径
            NSString *fileName = response.suggestedFilename;
            NSString *cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
            NSString *fullPath = [cachePath stringByAppendingPathComponent:fileName];
            
            //(1)创建输出流,并打开
            self.outStream = [[NSOutputStream alloc] initToFileAtPath:fullPath append:YES];
            [self.outStream open];
    }
    -(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
            //(2)使用输出流写数据
            [self.outStream write:data.bytes maxLength:data.length];
            //累加已经下载的文件数据大小
            self.currentLength += data.length;
            //计算文件的下载进度 = 已经下载的 / 文件的总大小
            self.progressView.progress = 1.0 * self.currentLength / self.totalLength;
    }
    -(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
            //(3)关闭输出流
            [self.outStream close];
    }
解决内存飙升

4. 常用操作:开始 | 暂停 | 取消 | 恢复

  • 对于下载状态的控制(dataTask 为定义的下载任务属性,将创建任务的代码写到懒加载中)

    • 开始下载
    [self.dataTask resume];
    
    • 暂停下载
    [self.dataTask suspend];
    
    • 恢复下载
    [self.dataTask resume];
    
    • 取消下载
    [self.dataTask cancel];
    //默认情况下取消下载不能进行恢复,若要取消之后还可以恢复,可以清空下载任务,再新建
    self.dataTask = nil;
    
下载控制效果

5. 断点下载

  • 在上面的效果图中,我们已经看到可以控制下载的状态,但是到最后又有了一个新的问题:下载进度值发生跳跃错乱
  • 原因分析:在前面计算进度值的时候,我们一直使用的方法是用已经下载的数据 / 文件的总数据,在第一个代理方法中,我们得到的文件大小并不是真正的文件大小,而是剩余未下载的大小,所以在第一次开始下载时,可以得到正确的数据,但是在下载过程中执行其他操作,就会使得到的数据大小发生变化,从而导致下载进度值出现问题
  • 解决方案:文件总大小 = 已经下载的数据 + 剩余未下载的数据
self.totalLength = response.expectedContentLength + self.currentLength;
  • 优化性能(以文件句柄方式为例,输出流同理):只有第一次接收到响应的时候才需要创建空的文件
if(self.currentLength == 0) {
        //在沙盒中创建一个空的文件
        [[NSFileManager defaultManager] createFileAtPath:fullPath contents:nil attributes:nil];
    }
  • 实现断点续传

    1. 在创建文件句柄后,更改文件句柄指向文件的末尾
    [self.handle seekToEndOfFile];
    
    1. 在请求头信息中添加需要请求的数据范围(从当前已经下载的数据末尾开始,到整个文件的末尾)
    NSString *rangeString = [NSString stringWithFormat:@"bytes=%zd-",self.currentLength];
    [request setValue:rangeString forHTTPHeaderField:@"Range"];
    
断点下载效果

6. 离线断点下载

  • 在用户日常使用的过程中,可能会出现下载文件到一半的时候,网络断开导致下载失败,为了避免重复下载,就要用到离线下载的功能
  • 做离线断点下载的主要步骤就是要到沙盒中获取到之前已经下载好的数据和数据的大小,因此发送请求开始下载之前,要先在 viewDidLoad 中做一些处理
//获得之前已经下载的文件数据大小 => 获得沙盒中已经存在的文件数据大小
//获得某个路径对应文件的属性
NSDictionary *fileInfo = [[NSFileManager defaultManager] attributesOfItemAtPath:fullPath error:nil];
self.currentLength = [fileInfo fileSize];
  • 此时离线断点下载的功能也已经做好,但是仍有一些小问题需要处理

  • 优化:再次打开程序时,进度条为空,开始下载时会直接跳到当前进度值,造成用户体验不好

  • 解决步骤:

    1. 在第一个代理方法中将文件的总大小写入到磁盘
    [[[NSString stringWithFormat:@"%zd",self.totalLength] dataUsingEncoding:NSUTF8StringEncoding] writeToFile:SizefullPath atomically:YES];
    
    1. 在 viewDidLoad 中做处理
    //显示文件的进度信息 = 已经下载文件数据大小(self.currentLength) / 文件的总大小
    NSData *totalSize = [NSData dataWithContentsOfFile:SizefullPath];
    self.totalLength = [[[NSString alloc]initWithData:totalSize encoding:NSUTF8StringEncoding] integerValue];
    if (self.totalLength != 0) {
            self.progressView.progress = 1.0 * self.currentLength/self.totalLength;
    }
    
离线断点下载效果

写在最后

以上就是使用 NSURLSession 实现离线断点下载的全部过程,由于个人水平有限,如有错误,敬请指正!如果觉得这篇文章对您有所帮助,请点击下方的喜欢或关注本人,谢谢您的支持!

  • 实现源码地址:https://github.com/mortal-master/BWOfflineDownload

你可能感兴趣的:(【补充】NSURLSession 详解离线断点下载的实现)