美文网首页将来跳槽用iOS接下来要研究的知识点GXiOS
iOS + AFN3.0 断点下载及异常中断处理

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

作者: YaoYaoX | 来源:发表于2017-08-03 20:51 被阅读860次

断点下载是很常见的一个需求,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掉了之后如何恢复下载

相关文章

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

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

  • lldb

    lldb命令 进入断点的几种方式 如红色部分2.jpg 异常中断 断点设置 break set -n moonTe...

  • AFN3.0前后的断点下载

    3.0前用AFHTTPRequestOperation 3.0后就没这么简单了,得多写点代码可参考:iOS + A...

  • NSURLSession

    文档参考 iOS使用NSURLSession进行下载(包括后台下载,断点下载) NSURLSessionDownl...

  • iOS数据后台下载不被系统关闭闭

    iOS使用NSURLSession进行下载(包括后台下载,断点下载)参考文档:http://www.jianshu...

  • iOS: 断点下载

    最近想到断点下载这个技术点, 然后学习尝试了一下,下面分享自己的一些心得. 上网找了很多demo, 大多用的是老版...

  • ios -断点下载

  • iOS怎么进行后台下载,断点下载

    iOS怎么进行后台下载,断点下载 从iOS7以来,苹果阿爸推出NSURLSession后,iOS现在可以实现真正的...

  • iOS断点续传

    基于iOS10、realm封装的下载器(支持存储读取、断点续传、后台下载、杀死APP重启后的断点续传等功能)。下载...

  • swift3 iOS断点续传下载工具

    XCDownloadTool for iOS swift3 iOS swift 断点续传下载工具,重启APP恢复临...

网友评论

    本文标题:iOS + AFN3.0 断点下载及异常中断处理

    本文链接:https://www.haomeiwen.com/subject/rjmelxtx.html