iOS + AFN3.0 断点下载及异常中断处理

断点下载是很常见的一个需求,AFN3.0 也为我们提供了下载的方法,但要实现断点下载,还需要我们自己另行处理。不过也可以用ASI下载,很方便。Demo

一、 AFN3.0 下载过程
  1. 创建NSURLSessionDownloadTask:两种方式(简写,具体查看api)

    1. -[AFURLSessionManager downloadTaskWithRequest...]
      普通下载
    2. -[AFURLSessionManager downloadTaskWithResumeData:resumeData...]:
      断点下载,resumeData是关键,没有就不能
    
  2. 开始下载

    1. [downloadTask resume]
    2. 下载时,会在tmp文件中生成下载的临时文件,
       文件名是CFNetworkDownload_XXXXXX.tmp,后缀由系统随机生成
    3. 下载完将临时文件移动到目的路径
    
  3. 暂停下载

    1. [downloadTask suspend]
    2. 暂停后task依然有效,通过resume又可以恢复下载
    
  4. 取消下载任务:取消后,task无效,要想继续下载,需要重新创建下载任务

    1. [downloadTask  cancle]:普通取消,无断点信息
    2. [downloadTask cancelByProducingResumeData...]
        1. 断点下载用,取消并返回断点信息,下次开启下载任务时传入  
        2. 取消任务时,只有满足以下的各条件,才会产生resumeData
           1. 自从资源开始请求后,资源未更改过
           2. 任务必须是 HTTP 或 HTTPS 的 GET 请求
           3. 服务器在response信息汇总提供了 ETag 或 Last-Modified头部信息
           4. 服务器支持 byte-range 请求
           5. 下载的临时文件未被删除
    
二、断点下载实现代码
  1. 新建下载任务

    + (NSURLSessionDownloadTask *)downloadTaskWithUrl:(NSString *)url
      destinationUrl:(NSString *)desUrl
      progress:(void (^)(NSProgress *))progressHandler
      complete:(MISDownloadManagerCompletion)completionHandler {
        // 检错
        if (!url || url.length < 1 || !desUrl || desUrl.length < 1) {
            NSError *error = [NSError errorWithDomain:@"参数不全" code:-1000 userInfo:nil];
            completionHandler(nil,nil,error);
            return nil;
        }
        
        // 参数
        NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]];
        NSURL *(^destination)(NSURL *, NSURLResponse *) =
        ^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {
            return [NSURL fileURLWithPath:desUrl];
        };
        
        // 1. 生成任务
        NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
        AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration];
        NSData *resumeData = [self getResumeDataWithUrl:url];
        if (resumeData) {
            // 1.1 有断点信息,走断点下载
            NSURLSessionDownloadTask *downloadTask =
            [manager downloadTaskWithResumeData:resumeData
                                       progress:progressHandler
                                    destination:destination
                                    completionHandler:completionHandler];
            // 删除历史恢复信息,重新下载后该信息内容已不正确,不再使用,
            [self removeResumeDataWithUrl:url];
            return downloadTask;
        } else {
            // 1.2 普通下载
            NSURLSessionDownloadTask *downloadTask =
            [manager downloadTaskWithRequest:request
                                    progress:progressHandler
                                 destination:destination
                           completionHandler:completionHandler];
            
            return downloadTask;
        }
    }
    
  2. 开始

     + (void)startDownloadTask:(NSURLSessionDownloadTask *) downloadTask {
         [downloadTask resume];
     }
    
  3. 暂停

     + (void)suspendDownloadTask:(NSURLSessionDownloadTask *) downloadTask {
         [downloadTask suspend];
     }
    
  4. 取消

    + (void)cancleDownloadTask:(NSURLSessionDownloadTask *) downloadTask {
         __weak typeof(task) weakTask = task;
         [downloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
             // 存储resumeData,以便一次进行断点下载
             [YYDownloadManager saveResumeData:resumeData withUrl:weakTask.currentRequest.URL.absoluteString];
         }];
     }
    
  5. 断点信息存储:代码太多,只列个思路供参考,需要的可以查看Demo

     1. 随机为resumeData分配一个文件名并储存到本地
     2. 用一个map文件记录特定url对应的resumeData位置,以便查找
    
     + (void)saveResumeData:(NSData *)resumeData withUrl:(NSString *)url{
         // 存储resumeData
     }
     + (NSData *)getResumeDataWithUrl:(NSString *)url {
         // 获取resumeData
     }
     + (void)removeResumeDataWithUrl:(NSString *)url {
         // resumeData无效之后应该删除
     }
    
三、问题及解决方案:获取resumeData

场景:以上的下载过程,只适合用户手动暂停的场景,当出现意外情况的时候,比如好奇点了小飞机🤡,手一抖kill掉了app💩,将无法获取到resumeData,也就无法断点下载,若刚好碰到下载一个超大的文件,那也就无奈了😹😹😹....用户当然也无法容忍这种情况发生。

解决方案:创建下载任务时,只提供了传入resumeData进行断点下载的方法,这大大简化了断点下载的过程,但同时又造成了很大的不便,当没有resumeData时,便无法断点下载,所以出现问题的解决办法就是获取resumeData。

1. 网络中断
1. downloadTask会中断,并返回错误信息,任务不能resume,若要继续需重建任务
2. 这种情况,查看错误信息会发现,里面有携带resumeDat
3. 那这就好办了,拿到resumeData并保存起来
4. 在(第一步)新建downloadTask时,有传入completionHandler,我们对其做一层处理
  
// 1.3 下载完成处理
MISDownloadManagerCompletion completeBlock =
^(NSURLResponse *response, NSURL *filePath, NSError *error) {
     // 任务完成或暂停下载
     if (!error || error.code == -999) {
        // 调用cancle的时候,任务也会结束,并返回-999错误,
        // 此时由于系统已返回resumeData,不另行处理了
        if (!error) {
            // 任务完成,移除resumeData信息
            [self removeResumeDataWithUrl:response.URL.absoluteString];
         }
         if (completionHandler) {
            completionHandler(response,filePath,error);
          }
      } else  {
          // 部分网络出错,会返回resumeData
          NSData *resumeData = error.userInfo[NSURLSessionDownloadTaskResumeData];
          [self saveResumeData:resumeData withUrl:response.URL.absoluteString];  
          if (completionHandler) {
              completionHandler(response,filePath,error);
           }
       }
 };
2. 意外kill掉了app
  1. 这种情况不好获取resumeData,也曾做过尝试,监听UIApplicationWillTerminateNotification的通知,在app要结束的时候获取resumeData并保存,但现实还是比较残酷,由于时间太短resumeData无法保存成功,不可行

  2. 既然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文件地址
  3. 回顾一下断点下载实际所需要的几要素

    1. 下载url
    2. 临时文件:即未完成的文件,断点下载开始后,需要继续将剩余文件流导入到临时文件尾部
    3. 文件开始位置:即临时文件大小,用于告诉服务器从哪块开始继续下载
    
  4. 🤓🤓🤓从2、3可以发现,resumeData其实就是一个包含了断点下载所需数据的一个plist文件...那就有思路了,何不尝试自己建一个resumeData呢?

  5. 尝试:按照上面resumeData的格式手动建一个plist文件,但只保留NSURLSessionDownloadURL、NSURLSessionResumeBytesReceived、NSURLSessionResumeInfoTempFileName三个字段,下载时加载该文件当成resumeData传入,开始下载任务........哈哈哈,竟然能成功进行断点下载

  6. 解决方案:分析后,发现可以自己伪造一个resumeData进行断点下载,只要拿到关键的几个数据

    1. 下载url:很方便能拿到
    2. 临时文件的path:由于其是系统自动下载,要拿到也需费一番功夫,地址隐藏在创建好的NSURLSessionDownloadTask对象中
    3. 已接收字节数:需拿到临时文件的字节数
代码实现

1.创建好普通下载任务后(非断点下载任务),
  从NSURLSessionDownloadTask中获取临时文件名,
  并存入到tempFile的map文件中

{       
    //****创建普通task时多加一步骤:获取tmp文件名并保存****
    // 1.2 创建普通下载任务
    downloadTask = [manager downloadTaskWithRequest:request
                                           progress:progressHandler
                                        destination:destination
                                  completionHandler:completeBlock];
    // 1.3 获取临时文件名,并保存
    NSString *tempFileName = [self getTempFileNameWithDownloadTask:downloadTask];
    [self saveTempFileName:tempFileName withUrl:url];
}

2. 获取临时文件名的代码
+ (NSString *)getTempFileNameWithDownloadTask:(NSURLSessionDownloadTask *)downloadTask {
     //NSURLSessionDownloadTask --> 属性downloadFile:__NSCFLocalDownloadFile --> 属性path
     NSString *tempFileName = nil;

     // downloadTask的属性(NSURLSessionDownloadTask) dt
     unsigned int dtpCount;
     objc_property_t *dtps = class_copyPropertyList([downloadTask class], &dtpCount);
     for (int i = 0; i<dtpCount; i++) {
        objc_property_t dtp = dtps[i];
        const char *dtpc = property_getName(dtp);
        NSString *dtpName = [NSString stringWithUTF8String:dtpc];
                    
        // downloadFile的属性(__NSCFLocalDownloadFile) df
        if ([dtpName isEqualToString:@"downloadFile"]) {
            id downloadFile = [downloadTask valueForKey:dtpName];
            unsigned int dfpCount;
            objc_property_t *dfps = class_copyPropertyList([downloadFile class], &dfpCount);
            for (int i = 0; i<dfpCount; i++) {
                objc_property_t dfp = dfps[i];
                const char *dfpc = property_getName(dfp);
                NSString *dfpName = [NSString stringWithUTF8String:dfpc];
                // 下载文件的临时地址
                if ([dfpName isEqualToString:@"path"]) {
                    id pathValue = [downloadFile valueForKey:dfpName];
                    NSString *tempPath = [NSString stringWithFormat:@"%@",pathValue];
                    tempFileName = tempPath.lastPathComponent;
                    break;
                 }
              }
              free(dfps);
              break;
          }
      }
      free(dtps);
      return tempFileName;
   }

3. 获取resumeData过程稍微调整
   1. 创建断点下载任务时,根据resumeDataMap找到resumeData,
   2. 若没发现resumeData,则根据tempFileMap的信息找到临时文件,
      获取其大小,然后尝试手动建一个resumeData,并加载到内存中
   3. 若没发现临时文件,则不创建resumeData,建立普通下载任务

/// 手动创建resume信息
+ (NSData *)createResumeDataWithUrl:(NSString *)url {
    if (url.length < 1) {
        return nil;
    }
                
    // 1. 从map文件中获取resumeData的name
    NSMutableDictionary *resumeMap = [NSMutableDictionary dictionaryWithContentsOfFile:[self resumeDataMapPath]];
    NSString *resumeDataName = resumeMap[url];
    if (resumeDataName.length < 1) {
        resumeDataName = [self getRandomResumeDataName];
        resumeMap[url] = resumeDataName;
        [resumeMap writeToFile:[self resumeDataMapPath] atomically:YES];
    }
                
     // 2. 获取断点下载的参数信息
     NSString *resumeDataPath = [self resumeDataPathWithName:resumeDataName];
     NSDictionary *tempFileMap = [NSDictionary dictionaryWithContentsOfFile:[self tempFileMapPath]];
     NSString *tempFileName = tempFileMap[url];
     if (tempFileName.length > 0) {
         NSString *tempFilePath = [self tempFilePathWithName:tempFileName];
         NSFileManager *fileMgr = [NSFileManager defaultManager];
        if ([fileMgr fileExistsAtPath:tempFilePath]) {
            // 获取文件大小
            NSDictionary *tempFileAttr = [[NSFileManager defaultManager] attributesOfItemAtPath:tempFilePath error:nil ];
            unsigned long long fileSize = [tempFileAttr[NSFileSize] unsignedLongLongValue];
                        
            // 3. 手动建一个resumeData
            NSMutableDictionary *fakeResumeData = [NSMutableDictionary dictionary];
            fakeResumeData[@"NSURLSessionDownloadURL"] = url;
            // ios8、与>ios9方式稍有不同
            if (NSFoundationVersionNumber >= NSFoundationVersionNumber_iOS_9_0) {
               fakeResumeData[@"NSURLSessionResumeInfoTempFileName"] = tempFileName;
            } else {
               fakeResumeData[@"NSURLSessionResumeInfoLocalPath"] = tempFilePath;
            }
            fakeResumeData[@"NSURLSessionResumeBytesReceived"] = @(fileSize);
            [fakeResumeData writeToFile:resumeDataPath atomically:YES];
                        
            // 重新加载信息
            return [NSData dataWithContentsOfFile:resumeDataPath];
         }
     }
     return nil;
 }

四、其他

  1. Demo中的测试地址
    是GitHub Desktop的下载地址,支持断点下载、下载完后打开文件可用于检验文件是否完整;文件比较大,可以模拟各个过程
  2. 既然可以自己造一个resumeData,为什么还用系统返回的数据?
    自己造的毕竟不规范,能用系统提供的尽量用系统提供的,也为了减少未知的错误

五、更新

iOS后台下载、断点下载:里面详细介绍了如何在app被kill掉了之后如何恢复下载

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

推荐阅读更多精彩内容