总结iOS开发中的断点续传与实践

前言

断点续传概述

断点续传就是从文件上次中断的地方开始重新下载或上传数据,而不是从文件开头。(本文的断点续传仅涉及下载,上传不在讨论之内)当下载大文件的时候,如果没有实现断点续传功能,那么每次出现异常或者用户主动的暂停,都会去重头下载,这样很浪费时间。所以项目中要实现大文件下载,断点续传功能就必不可少了。当然,断点续传有一种特殊的情况,就是 iOS 应用被用户 kill 掉或者应用 crash,要实现应用重启之后的断点续传。这种特殊情况是本文要解决的问题。

断点续传原理

要实现断点续传 , 服务器必须支持。目前最常见的是两种方式:FTP 和 HTTP

下面来简单介绍 HTTP 断点续传的原理。

HTTP

通过 HTTP,可以非常方便的实现断点续传。断点续传主要依赖于 HTTP 头部定义的 Range 来完成。在请求某范围内的资源时,可以更有效地对大资源发出请求或从传输错误中恢复下载。有了 Range,应用可以通过 HTTP 请求曾经获取失败的资源的某一个返回或者是部分,来恢复下载该资源。当然并不是所有的服务器都支持 Range,但大多数服务器是可以的。Range 是以字节计算的,请求的时候不必给出结尾字节数,因为请求方并不一定知道资源的大小。

Range 的定义如图 1 所示:

图 1. HTTP-Range

图 2 展示了 HTTP request 的头部信息:

图 2. HTTP request 例子

在上面的例子中的“Range: bytes=1208765-”表示请求资源开头 1208765 字节之后的部分。

图 3 展示了 HTTP response 的头部信息:

图 3. HTTP response 例子

上面例子中的”Accept-Ranges: bytes”表示服务器端接受请求资源的某一个范围,并允许对指定资源进行字节类型访问。”Content-Range: bytes 1208765-20489997/20489998”说明了返回提供了请求资源所在的原始实体内的位置,还给出了整个资源的长度。这里需要注意的是 HTTP return code 是 206 而不是 200。

断点续传分析 -AFHTTPRequestOperation

了解了断点续传的原理之后,我们就可以动手来实现 iOS 应用中的断点续传了。由于笔者项目的资源都是部署在 HTTP 服务器上 , 所以断点续传功能也是基于 HTTP 实现的。首先来看下第三方网络框架 AFNetworking 中提供的实现。清单 1 示例代码是用来实现断点续传部分的代码:

清单 1. 使用 AFHTTPRequestOperation 实现断点续传的代码

// 1 指定下载文件地址 URLString
 // 2 获取保存的文件路径 filePath
 // 3 创建 NSURLRequest
 NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:URLString]];
 unsigned long long downloadedBytes = 0; 

 if ([[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];

使用以上代码 , 断点续传功能就实现了,应用重新启动或者出现异常情况下 , 都可以基于已经下载的部分开始继续下载。关键的地方就是把已经下载的数据持久化。接下来简单看下 AFHTTPRequestOperation 是怎么实现的。通过查看源码 , 我们发现 AFHTTPRequestOperation 继承自 AFURLConnectionOperation , 而 AFURLConnectionOperation 实现了 NSURLConnectionDataDelegate 协议。

处理流程如图 4 所示:

图 4. AFURLHTTPrequestOperation 处理流程

这里 AFNetworking 为什么采取子线程调异步接口的方式 , 是因为直接在主线程调用异步接口 , 会有一个 Runloop 的问题。当主线程调用 [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES] 时 , 请求发出之后的监听任务会加入到主线程的 Runloop 中 ,RunloopMode 默认为 NSDefaultRunLoopMode, 这个表示只有当前线程的 Runloop 处理 NSDefaultRunLoopMode 时,这个任务才会被执行。而当用户在滚动 TableView 和 ScrollView 的时候,主线程的 Runloop 处于 NSEventTrackingRunLoop 模式下,就不会执行 NSDefaultRunLoopMode 的任务。

另外由于采取子线程调用接口的方式 , 所以这边的 DownloadProgressBlock,success 和 failure Block 都需要回到主线程来处理。

断点续传实战

了解了原理和 AFHTTPRequestOperation 的例子之后 , 来看下实现断点续传的三种方式:

NSURLConnection

基于 NSURLConnection 实现断点续传 , 关键是满足 NSURLConnectionDataDelegate 协议,主要实现了如下三个方法:

清单 2. NSURLConnection 的实现

 // SWIFT
 // 请求失败处理
 func connection(connection: NSURLConnection,
 didFailWithError error: NSError) {
  self.failureHandler(error: error)
 } 

 // 接收到服务器响应是调用
 func connection(connection: NSURLConnection,
 didReceiveResponse response: NSURLResponse) {
  if self.totalLength != 0 {
    return
  } 

  self.writeHandle = NSFileHandle(forWritingAtPath:
  FileManager.instance.cacheFilePath(self.fileName!)) 

  self.totalLength = response.expectedContentLength + self.currentLength
 } 

 // 当服务器返回实体数据是调用
 func connection(connection: NSURLConnection, didReceiveData data: NSData) {
  let length = data.length 

  // move to the end of file
  self.writeHandle.seekToEndOfFile() 

  // write data to sanbox
  self.writeHandle.writeData(data) 

  // calculate data length
  self.currentLength = self.currentLength + length 

  print("currentLength\(self.currentLength)-totalLength\(self.totalLength)") 

  if (self.downloadProgressHandler != nil) {
    self.downloadProgressHandler(bytes: length, totalBytes:
    self.currentLength, totalBytesExpected: self.totalLength)
  }
 } 

 // 下载完毕后调用
 func connectionDidFinishLoading(connection: NSURLConnection) {
  self.currentLength = 0
  self.totalLength = 0 

  //close write handle
  self.writeHandle.closeFile()
  self.writeHandle = nil 

  let cacheFilePath = FileManager.instance.cacheFilePath(self.fileName!)
  let documenFilePath = FileManager.instance.documentFilePath(self.fileName!) 

  do {
    try FileManager.instance.moveItemAtPath(cacheFilePath, toPath: documenFilePath)
  } catch let e as NSError {
    print("Error occurred when to move file: \(e)")
  } 

  self.successHandler(responseObject:fileName!)
 }

如图 5 所示 , 说明了 NSURLConnection 的一般处理流程。

图 5. NSURLConnection 流程

根据图 5 的一般流程,在 didReceiveResponse 中初始化 fileHandler, 在 didReceiveData 中 , 将接收到的数据持久化的文件中 , 在 connectionDidFinishLoading 中,清空数据和关闭 fileHandler,并将文件保存到 Document 目录下。所以当请求出现异常或应用被用户杀掉,都可以通过持久化的中间文件来断点续传。初始化 NSURLConnection 的时候要注意设置 scheduleInRunLoop 为 NSRunLoopCommonModes,不然就会出现进度条 UI 无法更新的现象。

实现效果如图 6 所示:

图 6. NSURLConnection 演示

NSURLSessionDataTask

苹果在 iOS7 开始,推出了一个新的类 NSURLSession, 它具备了 NSURLConnection 所具备的方法,并且更强大。由于通过 NSURLConnection 从 2015 年开始被弃用了,所以读者推荐基于 NSURLSession 去实现续传。NSURLConnection 和 NSURLSession delegate 方法的映射关系 , 如图 7 所示。所以关键是要满足 NSURLSessionDataDelegate 和 NSURLsessionTaskDelegate。

图 7. 协议之间映射关系

代码如清单 3 所示 , 基本和 NSURLConnection 实现的一样。

清单 3. NSURLSessionDataTask 的实现

 // SWIFT
 // 接收数据
 func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask,
 idReceiveData data: NSData) {
  //. . .
 }
 // 接收服务器响应
 func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask,
 didReceiveResponse response: NSURLResponse, completionHandler:
 (NSURLSessionResponseDisposition) -> Void) {
  // . . .
  completionHandler(.Allow)
 } 

 // 请求完成
 func URLSession(session: NSURLSession, task: NSURLSessionTask,
 didCompleteWithError error: NSError?) {
  if error == nil {
    // . . .
    self.successHandler(responseObject:self.fileName!)
  } else {
    self.failureHandler(error:error!)
  }
 }

区别在与 didComleteWithError, 它将 NSURLConnection 中的 connection:didFailWithError:

connectionDidFinishLoading: 整合到了一起 , 所以这边要根据 error 区分执行成功的 Block 和失败的 Block。

实现效果如图 8 所示:

图 8. NSURLSessionDataTask 演示

NSURLSessionDownTask

最后来看下 NSURLSession 中用来下载的类 NSURLSessionDownloadTask,对应的协议是 NSURLSessionDownloadDelegate,如图 9 所示:

图 9. NSURLSessionDownloadDelegate 协议

其中在退出 didFinishDownloadingToURL 后,会自动删除 temp 目录下对应的文件。所以有关文件操作必须要在这个方法里面处理。之前笔者曾想找到这个 tmp 文件 , 基于这个文件做断点续传 , 无奈一直找不到这个文件的路径。等以后 SWIFT 公布 NSURLSession 的源码之后,兴许会有方法找到。基于 NSURLSessionDownloadTask 来实现的话 , 需要在 cancelByProducingResumeData 中保存已经下载的数据。进度通知就非常简单了,直接在 URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesWritten:totalBytesExpectedToWrite: 实现即可。

代码如清单 4 所示:

清单 4. NSURLSessionDownloadTask 的实现

 //SWIFT 

 //UI 触发 pause
 func pause(){
  self.downloadTask?.cancelByProducingResumeData({data -> Void in
    if data != nil {
 data!.writeToFile(FileManager.instance.cacheFilePath(self.fileName!),
 atomically: false)
 }
    })
  self.downloadTask = nil
 } 

 // MARK: - NSURLSessionDownloadDelegate
 func URLSession(session: NSURLSession, downloadTask:
 NSURLSessionDownloadTask, didWriteData bytesWritten: Int64,
 totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
  if (self.downloadProgressHandler != nil) {
    self.downloadProgressHandler(bytes: Int(bytesWritten),
     totalBytes: totalBytesWritten, totalBytesExpected: totalBytesExpectedToWrite)
  }
 } 

 func URLSession(session: NSURLSession, task: NSURLSessionTask,
 didCompleteWithError error: NSError?) {
  if error != nil {//real error
    self.failureHandler(error:error!)
  }
 } 

 func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask,
 didFinishDownloadingToURL location: NSURL) {
  let cacheFilePath = FileManager.instance.cacheFilePath(self.fileName!)
  let documenFilePath = FileManager.instance.documentFilePath(self.fileName!)
  do {
    if FileManager.instance.fileExistsAtPath(cacheFilePath){
      try FileManager.instance.removeItemAtPath(cacheFilePath)
    }
    try FileManager.instance.moveItemAtPath(location.path!, toPath: documenFilePath)
  } catch let e as NSError {
    print("Error occurred when to move file: \(e)")
  }
  self.successHandler(responseObject:documenFilePath)
 }

实现效果如图 10 所示:

图 10. NSURLSessionDownloadTask 演示

总结

以上就是本文总结iOS开发中的断点续传与实践的全部内容,其实,下载的实现远不止这些内容,本文只介绍了简单的使用。希望在进一步的学习和应用中能继续与大家分享。希望本文能帮助到有需要的大家。

(0)

相关推荐

  • php断点续传之如何分割合并文件

    复制代码 代码如下: <?php ini_set("memory_limit", "50M");//必须的,根据你环境的实际情况尽量大,防止报错 ini_set("max_execution_time", "100"); //file_exists() 函数检查文件或目录是否存在,存在则返回 true,否则返回 false. //fread() 函数读取文件(可安全用于二进制文件).fread() 从文件指针 file

  • php下载远程文件类(支持断点续传)

    简易使用方法:  复制代码 代码如下: $object = new httpdownload(); $object->set_byfile($file)%N#H#%;//服务器文件名,包括路径 $object->filename = $filename;//下载另存为的文件名 $object->download(); 3.源文件: 复制代码 代码如下: <? class httpdownload { var $data = null; var $data_len = 0; var

  • C#实现支持断点续传多线程下载客户端工具类

    复制代码 代码如下: /* .Net/C#: 实现支持断点续传多线程下载的 Http Web 客户端工具类 (C# DIY HttpWebClient) * Reflector 了一下 System.Net.WebClient ,改写或增加了若干: * DownLoad.Upload 相关方法! * DownLoad 相关改动较大! * 增加了 DataReceive.ExceptionOccurrs 事件! * 了解服务器端与客户端交互的 HTTP 协议参阅: * 使文件下载的自定义连接支持

  • java断点续传功能实例(java获取远程文件)

    复制代码 代码如下: import java.io.BufferedInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.RandomAccessFile; import java.net . * ; /** * 文件传送客户端:获取远程文件 */ public cl

  • iOS开发网络编程之断点续传

    前言 网络下载是我们在项目中经常要用到的功能,如果是小文件的下载,比如图片和文字之类的,我们可以直接请求源地址,然后一次下载完毕.但是如果是下载较大的音频和视频文件,不可能一次下载完毕,用户可能下载一段时间,关闭程序,回家接着下载.这个时候,就需要实现断点续传的功能.让用户可以随时暂停下载,下次开始下载,还能接着上次的下载的进度. 今天我们来看看如何自己简单的封装一个断点续传的类,实现如下功能. 1.使用者只需要调用一个接口即可以下载,同时可以获取下载的进度. 2.下载成功,可以获取文件存储的位

  • Java如何实现HTTP断点续传功能

    (一)断点续传的原理 其实断点续传的原理很简单,就是在Http的请求上和一般的下载有所不同而已. 打个比方,浏览器请求服务器上的一个文时,所发出的请求如下: 假设服务器域名为wwww.sjtu.edu.cn,文件名为down.zip. GET /down.zip HTTP/1.1 Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms- excel, application/msword, a

  • c#实现断点续传功能示例分享

    在了解HTTP断点续传的原理之前,让我们先来了解一下HTTP协议,HTTP协议是一种基于tcp的简单协议,分为请求和回复两种.请求协议是由客户机(浏览器)向服务器(WEBSERVER)提交请求时发送报文的协议.回复协议是由服务器(webserver),向客户机(浏览器)回复报文时的协议.请求和回复协议都由头和体组成.头和体之间以一行空行为分隔. 以下是一个请求报文与相应的回复报文的例子: 复制代码 代码如下: GET /image/index_r4_c1.jpg HTTP/1.1Accept:

  • php实现的支持断点续传的文件下载类

    本文实例讲述了php实现的支持断点续传的文件下载类及其用法,是非常实用的技巧.分享给大家供大家参考.具体方法如下: 通常来说,php支持断点续传,主要依靠HTTP协议中 header HTTP_RANGE实现. HTTP断点续传原理: Http头 Range.Content-Range() HTTP头中一般断点下载时才用到Range和Content-Range实体头, Range用户请求头中,指定第一个字节的位置和最后一个字节的位置,如(Range:200-300) Content-Range用

  • python支持断点续传的多线程下载示例

    复制代码 代码如下: #! /usr/bin/env python#coding=utf-8 from __future__ import unicode_literals from multiprocessing.dummy import Pool as ThreadPoolimport threading import osimport sysimport cPicklefrom collections import namedtupleimport urllib2from urlparse

  • 总结iOS开发中的断点续传与实践

    前言 断点续传概述 断点续传就是从文件上次中断的地方开始重新下载或上传数据,而不是从文件开头.(本文的断点续传仅涉及下载,上传不在讨论之内)当下载大文件的时候,如果没有实现断点续传功能,那么每次出现异常或者用户主动的暂停,都会去重头下载,这样很浪费时间.所以项目中要实现大文件下载,断点续传功能就必不可少了.当然,断点续传有一种特殊的情况,就是 iOS 应用被用户 kill 掉或者应用 crash,要实现应用重启之后的断点续传.这种特殊情况是本文要解决的问题. 断点续传原理 要实现断点续传 , 服

  • iOS开发中#import、#include和@class的区别解析

    1. 一般来说,导入objective c的头文件时用#import,包含c/c++头文件时用#include. 2. #import 确定一个文件只能被导入一次,这使你在递归包含中不会出现问题.<标记> 所以,#import比起#include的好处就是不会引起交叉编译. #import && #class: 1. import会包含这个类的所有信息,包括实体变量和方法(.h文件中),而@class只是告诉编译器,其后面声明的名称是类的名称,至于这些类是如何定义的,后面会再告

  • IOS开发中加载大量网络图片优化方法

    IOS开发中加载大量网络图片如何优化 1.概述 在IOS下通过URL读一张网络图片并不像其他编程语言那样可以直接把图片路径放到图片路径的位置就ok,而是需要我们通过一段类似流的方式去加载网络图片,接着才能把图片放入图片路径显示.比如: -(UIImage *) getImageFromURL:(NSString *)fileURL { //NSLog(@"执行图片下载函数"); UIImage * result; NSData * data = [NSData dataWithCont

  • 浅谈iOS开发中static变量的三大作用

    (1)先来介绍它的第一条也是最重要的一条:隐藏 当我们同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性.为理解这句话,我举例来说明.我们要同时编译两个源文件,一个是a.c,另一个是main.c. 下面是a.c的内容 char a = 'A'; // global variable void msg() { printf("Hello\n"); } 下面是main.c的内容 int main(void) { extern char a; // extern v

  • 详解IOS开发中生成推送的pem文件

    详解IOS开发中生成推送的pem文件 具体步骤如下: 首先,需要一个pem的证书,该证书需要与开发时签名用的一致. 具体生成pem证书方法如下: 1. 登录到 iPhone Developer Connection Portal(http://developer.apple.com/iphone/manage/overview/index.action )并点击 App IDs 2. 创建一个不使用通配符的 App ID .通配符 ID 不能用于推送通知服务.例如,  com.itotem.ip

  • iOS开发中AVPlayer的简单应用

    前言 在iOS开发中,播放视频通常有两种方式,一种是使用MPMoviePlayerController(需要导入MediaPlayer.Framework),还有一种是使用AVPlayer.关于这两个类的区别简而言之就是MPMoviePlayerController使用更简单,功能不如AVPlayer强大,而AVPlayer使用稍微麻烦点,不过功能更加强大.下面这篇文章主要介绍下AVPlayer的简单应用,需要的朋友们一起来看看吧. AVPlayer的简单应用 1.引入系统框架 2.创建视频的u

  • IOS开发中禁止NavigationController的向右滑动返回

    IOS开发中禁止NavigationController的向右滑动返回 大家在进行开法的时候细心的朋友会发现,.用后在屏幕的最左边,向右滑动,,你会发现,你的App返回到了上一个页面,这是怎么回事呢, 在你的App中输入UINavigationController ,然后按住commend键,点击鼠标,跳进去,如下图: 在UINavigationController 的属性中你会发现 红色下划线部分,你看到了UINavigationController 自带的有一个panGeesture,所以,

  • iOS开发中如何实现一个平滑的颜色过渡

    如何从A颜色平滑的过渡到B颜色 同一个视图,随着进度或者其他过程的变化,从A颜色过渡到B颜色. 所有的颜色都是是由RGB三原色组成,在iOS开发中,通过RGB值的组合来显示不同的颜色.一次A-B其实就是RGB值的改变,如何平滑的过渡呢?那就是等比例的改变RGB值! 那么,如何获取颜色的RGB值呢? - (NSArray *)getRGBDictionaryByColor:(UIColor *)originColor { CGFloat r=0,g=0,b=0,a=0; if ([self res

  • IOS 开发中画扇形图实例详解

    IOS 开发中画扇形图实例详解 昨天在做项目中,遇到一个需要显示扇形图的功能,网上搜了一下,发现code4app里面也没有找到我想要的那种类似的效果,没办法了,只能自己学习一下如何画了. 首先我们需要了解一个uiview的方法 -(void)drawRect:(CGRect)rect 我们知道了这个方法,就可以在自定义UIView的子类的- (void)drawRect:(CGRect)rect里面绘图了,关于drawrect的调用周期,网上也是一找一大堆,等下我会整理一下,转载一篇供你们参考.

  • iOS开发中以application/json上传文件实例详解

    本文通过实例代码给大家讲解iOS中以application/json上传文件的形式,具体内容详情大家参考下本文. 在和sever后台交互的过程中.有时候.他们需要我们iOS开发者以"application/json"形式上传. NSString *accessUrl = [NSString stringWithFormat:@"%@/xxx",@"https://www.xxxxx.com:xxxx"]; NSMutableURLRequest

随机推荐