总结iOS开发中的断点续传那些事儿

smra3417 5年前
   <h2>前言</h2>    <h3>断点续传概述</h3>    <p>断点续传就是从文件赏赐中断的地方重新开始下载或者上传数据,而不是从头文件开始。当下载大文件的时候,如果没有实现断点续传功能,那么每次出现异常或者用户主动的暂停,都会从头下载,这样很浪费时间有木有。所以呢,项目中实现大文件下载的时候,断点续传功能是必不可少了。当然咯,断点续传有一种特殊的情况,就是我们的应用呗用户kill掉或者应用crash,要实现应用重启之后的断点续传,这种情况就是我们将要解决的问题。</p>    <h3>断点续传的原理</h3>    <p>要实现断点续传,服务器必须是要支持的。目前最常见的两种方式: FTP 和 HTTP 。</p>    <p>下面来简单介绍HTTP断点续传的原理。</p>    <p>HTTP</p>    <p>通过HTTP,可以非常方便的实现断点续传。断点续传主要依赖于HTTP头部定义的Range,应用可以通过HTTP请求曾经获取失败的资源的某一个返回或者部分来恢复下载该资源。当然并不是所有风服务器都支持Range,所以不支持Range的不在我们考虑之内。Range是以字节计算的,请求的时候不比给我结尾字节数,因为请求方并不一定知道资源的大小。</p>    <p>通过这个关键字可以告诉服务器返回哪些数据给我。</p>    <p>比如:</p>    <p>bytes=500-999 表示第500-第999字节</p>    <p>bytes=500- 表示从第500字节往后的所有字节</p>    <p>然后我们再根据服务器返回的数据,将得到的data数据拼接到文件后面,就可以实现断点续传了。</p>    <h3>断点续传分析—AFHTTPRequestOperation</h3>    <p>在了解了断点续传的原理之后,我们就可以动手来实现iOS中的断点续传了。由于我现在接触到的项目都是部署在HTTP服务器上的,所以断点续传功能也基于HTTP实现。首先我们来最简单的入手,第三方神奇AFNetworking中提供的实现,下面请看详细代码:</p>    <pre>  <code class="language-objectivec">//1.指定下载文件的地址URLString   //2.获取保存的文件路径filePath   //3.获得NSURLRquest      NSString* URLString = @"";      NSString* filePath = @"";        NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:URLString]];      signed long long downloadBytes = 0;      ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {          // 3.1 若之前下载过 , 则在 HTTP 请求头部加入 Range          // 获取已下载文件的 size          downloadedBytes = [self fileSizeForPath:filePath];            // 验证是否下载过文件          if (downloadedBytes > 0) {              // 若下载过 , 断点续传的时候修改 HTTP 头部部分的 Range              NSMutableURLRequest *mutableURLRequest = [request mutableCopy];              NSString *requestRange =              [NSString stringWithFormat:@"bytes=%llu-", downloadedBytes];              [mutableURLRequest setValue:requestRange forHTTPHeaderField:@"Range"];              request = mutableURLRequest;          }      }        // 4 创建 AFHTTPRequestOperation      AFHTTPRequestOperation *operation      = [[AFHTTPRequestOperation alloc] initWithRequest:request];        // 5 设置操作输出流 , 保存在第 2 步的文件中      operation.outputStream = [NSOutputStream                                outputStreamToFileAtPath:filePath append:YES];        // 6 设置下载进度处理 block      [operation setDownloadProgressBlock:^(NSUInteger bytesRead,                                            long long totalBytesRead, long long totalBytesExpectedToRead) {          // bytesRead 当前读取的字节数          // totalBytesRead 读取的总字节数 , 包含断点续传之前的          // totalBytesExpectedToRead 文件总大小      }];         // 7 设置 success 和 failure 处理 block       [operation setCompletionBlockWithSuccess:^(                                                 AFHTTPRequestOperation *operation,                                                 id responseObject) {          }                                failure:^(AFHTTPRequestOperation *operation,                                                 NSError *error) {        }];        // 8 启动 operation      [operation start];</code></pre>    <p>使用以上代码 , 断点续传功能就实现了,应用重新启动或者出现异常情况下 , 都可以基于已经下载的部分开始继续下载。关键的地方就是把已经下载的数据持久化。接下来简单看下 AFHTTPRequestOperation 是怎么实现的。通过查看源码 , 我们发现  AFHTTPRequestOperation 继承自  AFURLConnectionOperation ,而 AFURLConnectionOperation 实现了  NSURLConnectionDataDelegate 协议。</p>    <p>处理流程如图所示:</p>    <p>可以看到,这里的AFNetworking采取自线程调一步接口的方式,是因为直接在主线程调用异步接口会有一个Runloop的问题。当主线程调用 [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES] 时,请求发出之后的监听任务会加到主线程中的Runloop中,我们知道RunloopMode默认为NSDefuaultRunloopMode,这个表示只有当前线程的Runloop处理NSDefaultRunloopMode时,这个任务才会执行。而当用户正在滚动tableview和scrollview的时候,主线程的Runloop处于NSEventTrackingRunloop模式下,就不会执行NSDefaultRunloopMode的任务。</p>    <p>另外由于采取子线程调用接口的方式,所以这边的DownloadProgressBlock,success 和 failure Block 都需要回到主线程来处理。</p>    <h3>断点续传实战</h3>    <p>NSURLConnecttion的实现</p>    <p>NSURLConnecttion这家伙已经在2015年就已经被苹果遗弃,所以在这里我们不做过多讨论,请注意啊,我是省略号……</p>    <p>NSURLSessionDataTask</p>    <p>苹果在iOS7开始,推出一个新的类 NSURLSession ,它具备了 NSURLConnection 所具备的方法,并且更加强大。所以我更加推荐大家使用这个类去实现下载和续传。NSURLConnection 和 NSURLSession delegate 方法的映射关系 , 如下图所示。所以关键是要满足 NSURLSessionDataDelegate 和 NSURLsessionTaskDelegate。</p>    <p>文件下载和暂停的实现</p>    <p>当使用NSURLSessionDownloadTask进行下载的时候,系统会在cache文件夹下创建一个下载的路径,路径下会有一个以”CFNetworking”打头的.tmp文件(以下简称”下载文件”防止混淆),这个就是我们正在下载中的文件。而当我们调用了cancelByProducingResumeData:方法后,会得到一个data文件,通过String格式化后,发现是一个XML文件,里面包含了关于.tmp文件的一些关键点的描述,包括”Range”,”key”,”下载文件的路径”等等.而原本存在于download文件下的下载文件,则被移动到了系统tmp文件夹目录下.而当我们再次进行resume操作的时候,下载文件则又被移回到了download文件夹下。</p>    <p>程序被杀掉的断点续传resumeData</p>    <p>根据上面的分析,基本可以得到以下结论:</p>    <ul>     <li> <p>DownloadTask每次进行断点续传的时候,会根据data文件中的”路径Key”去寻找下载文件,然后校验后再根据”Range”属性去进行断点续传。</p> </li>     <li> <p>download文件夹中存放的只会是下载中的文件,一旦暂停就会被移动到tmp文件夹下。</p> </li>     <li> <p>每个暂停得到的data文件,与下载文件一一对应。</p> </li>     <li> <p>断点续传只与tmp文件夹中的文件有关。</p> </li>    </ul>    <p>所以我们可以这么做,设置一个Bool变量用来判断是否正在下载中,同时用一个周期事件每隔一段时间暂停一次。然后保存data文件和拷贝tmp文件夹下的下载文件到安全目录下(因为tmp文件夹据说随时可能清空)。</p>    <p>当再次下载的时候,先是从安全目录下取到下载文件,删除tmp文件夹中原有的同名文件,然后copy到tmp目录下,最后利用保存的data文件进行再次downloadTaskWithResumeData操作,就可以实现再次下载了。</p>    <p> </p>    <p>来自:http://www.colotu.com/html/kjj/182.html</p>