iOS断点下载、kill掉APP可继续下载

一、 AFN3.0 下载过程

  1. 第一步肯定是创建AFURLSessionManager,配置一些NSURLSessionConfiguration,这一步我就不做多的叙述了。

不过因为我们是断点下载,可以在不同的地方都调用,而且为了调用方便,我们直接提供类(+)方法,但有一些成员变量可以重复使用,例如AFURLSessionManager,综合考虑,将折现成员变量声明称静态变量,并在+initialize方法里使用dispatch_once初始化。

  1. 创建NSURLSessionDownloadTask:

    - (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request
                                          progress:(void (^)(NSProgress *downloadProgress)) downloadProgressBlock
                                       destination:(NSURL * (^)(NSURL *targetPath, NSURLResponse *response))destination
                                 completionHandler:(void (^)(NSURLResponse *response, NSURL *filePath, NSError *error))completionHandler {}
    
    - (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData
                                             progress:(void (^)(NSProgress *downloadProgress)) downloadProgressBlock
                                          destination:(NSURL * (^)(NSURL *targetPath, NSURLResponse *response))destination
                                    completionHandler:(void (^)(NSURLResponse *response, NSURL *filePath, NSError *error))completionHandler {}
    

我们可以看到AFN给我们提供了两个创建NSURLSessionDownloadTask的方法,第一种方式是通过NSURLRequest创建,第二种方式是通过NSData创建。通过参数我们也能看出来,第一种是为了第一次下载提供的,第二种是为了我们做断点下载提供的。

  1. 开始下载

    [task resume];
    下载时,会在tmp文件中生成下载的临时文件,
    文件名是CFNetworkDownload_XXXXXX.tmp,后缀由系统随机生成
    下载完将临时文件移动到目的路径,路径为创建DownloadTask时传入的destination参数
    
  2. 暂停下载

    [task suspend]
    暂停后task依然有效,通过resume又可以恢复下载
    
  3. 取消下载任务,取消下载任务,当前的task会失效,如果想继续下载,需要重新创建下载任务

    [task cancel]
    [task cancelByProducingResumeData:^(NSData * _Nullable resumeData) {}]
    1. 第二种手动取消任务,会返回一个resumeData,这个参数是我们做断点下载所必须的,我们可以直接通过resumeData开启一个新的下载任务,发起下载请求
    2. 取消任务时,只有满足以下的各条件,才会产生resumeData
        1. 自从资源开始请求后,资源未更改过
        2. 任务必须是 HTTP 或 HTTPS 的 GET 请求
        3. 服务器在response信息汇总提供了 ETag 或 Last-Modified头部信息
        4. 服务器支持 byte-range 请求
        5. 下载的临时文件未被删除
    

二、断点下载的实现

源自网络:下载交互过程顺序图
  1. 为什么会出现断点下载,我分了三种情况

    1. 手动取消,也就是我们提供给用户或我们项目内部调用了1.5中的cancel方法
    2. 网络、服务器异常,导致下载失败
    3. 用户手动kill掉APP
  2. 第二情况的断点下载实现(部分网络错误):

     JQDownloadManagerCompletion completeBlock = ^(NSURLResponse *response, NSURL *filePath, NSError *error) {
         if (!error) { // 任务完成或暂停下载
             [self removeResumeDataWithUrl:url];
             [self removeTaskWithUrl:url];
         } else  { // 部分网络出错,会返回resumeData
             NSData *resumeData = error.userInfo[NSURLSessionDownloadTaskResumeData];
             if (resumeData) [self saveResumeData:resumeData url:url];
         }
     }
    

部分网络错误,在error.userInfo中我们是可以获取到resumeData,可以直接用于断点下载。

  1. 第三种情况最复杂

    1. 尝试在网络失败时获取resumeData,由于时间太短,不可行

    2. 尝试通过监听UIApplicationWillTerminateNotification的通知,在app要结束的时候获取resumeData并保存,但现实还是比较残酷,由于时间太短还是无法获取resumeData,不可行

    3. 从resumeData入手,进行解析

      <?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://downloadUrl</string>
           <key>NSURLSessionResumeBytesReceived</key>
               <integer>1474327</integer>
           <key>NSURLSessionResumeCurrentRequest</key>
               <data>
                ......
               </data>
           <key>NSURLSessionResumeEntityTag</key>
               <string>"XXXXXXXXXX"</string>
           <key>NSURLSessionResumeInfoTempFileName</key>
               <string>CFNetworkDownload_XXXXX.tmp</string>
           <key>NSURLSessionResumeInfoVersion</key>
               <integer>2</integer>
           <key>NSURLSessionResumeOriginalRequest</key>
               <data>
                .....
               </data>
           <key>NSURLSessionResumeServerDownloadDate</key>
                <string>week, dd MM yyyy hh:mm:ss </string>
      </dict></plist>
      
      1. 上面就是解析resumeData之后的数据,其实就是一个plist文件,里面信息包括了下载URL、已接收字节数、临时的下载文件名(文件默认存在tmp文件夹中)、当前请求、原始请求、下载事件、resumeInfo版本、EntityTag这些数据
      2. iOS8生成的resumeData稍有不同,没有NSURLSessionResumeInfoTempFileName字段,有NSURLSessionResumeInfoLocalPath,记录了完整的tmp文件地址
  2. 主要需要几个参数:下载URL、当前请求、已接收字节数、临时的下载文件名(文件默认存在tmp文件夹中)这四个数据

    1. 下载URL:已知
    2. 当前请求:需要通过已经下载的大小和URL创建
    3. 已接收字节数:需要通过临时文件来获取大小
    4. 临时文件:存放在本地tmp文件夹下,但由于文件名CFNetworkDownload_XXXXXX.tmp,是系统随机生成的,我们无法将tmp文件和URL对应。
  3. 获取tmp文件路径

    1. 手动cancel,在继续任务,在cancel回调中可以获取到resumeData,里面直接包含所有信息,我们只需要把数据中的字节数和当前请求更换就可以。
    2. 上一种方法,有显而易见的缺点,性能时间都会浪费,后来通过调试,查看信息,发现NSURLSessionDownloadTask中有个数据downloadFile存放了一些关于下载的信息,其中一个信息path就是存放临时文件路径的,通过lastPathComponent就可以直接取到相应的临时文件名。
    3. 通过tmp文件名获取tmp文件路径,这样做是因为本地文件路径会变,所以不能直接存task中的文件路径,需要获取到文件名,通过tmp的路径获取到tmp文件路径
  4. 生成resumeData

     NSData *resumeData;
     NSFileManager *fileMgr = [NSFileManager defaultManager];
     if ([fileMgr fileExistsAtPath:tempFilePath]) {
         NSDictionary *tempFileAttr = [[NSFileManager defaultManager] attributesOfItemAtPath:tempFilePath error:nil ];
         unsigned long long fileSize = [tempFileAttr[NSFileSize] unsignedLongLongValue];
     
         if (fileSize > 0) {
             NSMutableDictionary *fakeResumeData = [NSMutableDictionary dictionary];
         
             NSMutableURLRequest *newResumeRequest =[NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]];
             NSString *bytesStr =[NSString stringWithFormat:@"bytes=%ld-",fileSize];
             [newResumeRequest addValue:bytesStr forHTTPHeaderField:@"Range"];
         
             NSData *newResumeData =[NSKeyedArchiver archivedDataWithRootObject:newResumeRequest];
             [fakeResumeData setObject:newResumeData forKey:@"NSURLSessionResumeCurrentRequest"];
             [fakeResumeData setObject:url forKey:@"NSURLSessionDownloadURL"];
             [fakeResumeData setObject:@(fileSize) forKey:@"NSURLSessionResumeBytesReceived"];
             [fakeResumeData setObject:[tempFilePath lastPathComponent] forKey:@"NSURLSessionResumeInfoTempFileName"]; // iOS9以下 需要路径
    
             resumeData = [NSPropertyListSerialization dataWithPropertyList:fakeResumeData format:NSPropertyListXMLFormat_v1_0 options:0 error:nil];
         }
     }
    
  5. 大功告成,如果获取到resumeData,可以直接通过resumeData创建task,进行断点下载了。

  6. 下载完成,删除相应的数据

三、断点下载中涉及其他的知识点

  1. 数据缓存:

    1. 正在下载的URL
    2. 正在下载的URL的回调函数
    3. 下载的URL对应的resumeData
    4. 下载的URL对应的tmp文件名

    1 2 因为肯定是APP本次启动之后才存在的数据,所以直接使用静态变量存储就可以。
    3 4 则需要本地化,使用了JQCache存放在Document目录下

  2. 数据安全问题:

    1. 创建一个dispatch_queue
    2. 使用读写锁来保障数据安全
      1. dispatch_barrier_async
      2. dispatch_sync
    3. 还有互斥锁和自旋锁
    4. 信号量,如果有大量下载同时访问,需要控制并发数量,这里我采用了信号量的方式去控制。
  3. 同一URL处理

    1. 针对同一URL的网络请求,如果有下载任务,则不在此下载,判断方式通过参考SD的原理,考虑如果URL过长,全部对URL进行MD5处理,在进行判断和存储
    2. 不进行下载,但需要保存当前请求的回调函数,参考了关联对象的实现逻辑,对URL的回调函数做处理

断点下载Demo

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

推荐阅读更多精彩内容