【补充】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 实现离线断点下载的全部过程,由于个人水平有限,如有错误,敬请指正!如果觉得这篇文章对您有所帮助,请点击下方的喜欢或关注本人,谢谢您的支持!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容