您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
   
 
 
     
   
 订阅
  捐助
浅析 iOS 应用开发中的断点续传
 
来源:IBM  发布于: 2016-11-14
   次浏览      
 

本文先从断点续传问题开始,介绍断点续传概述和原理。接着结合笔者调研中尝试的 AFHTTPRequestOpeartion,简单分析源码。最后分别基于 NSURLConnection,NSURLSessionDataTask 和 NSURLSessionDownloadTask 去实现应用重启情况下的断点续传。

断点续传概述

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

断点续传原理

要实现断点续传 , 服务器必须支持。目前最常见的是两种方式:FTP 和 HTTP。下面来简单介绍 HTTP 断点续传的原理。

HTTP

通过 HTTP,可以非常方便的实现断点续传。断点续传主要依赖于 HTTP 头部定义的 Range 来完成。具体 Range 的说明参见 RFC2616中 14.35.2 节,在请求某范围内的资源时,可以更有效地对大资源发出请求或从传输错误中恢复下载。有了 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 演示

总结

本文从断点续传概述开始,介绍了断点续传的应用背景,通过原理的描述,相信读者对断点续传有了基本的认识和理解。接着笔者介绍了通过 AFHTTPRequestOpeartion 实现的代码,并对 AFHTTPRequestOpeartion 做了简单的分析。最后笔者结合的实际需求,基于 NSURLConnection, NSURLSeesionDataTask 和 NSURLSessionDownloadtask。其实,下载的实现远不止这些内容,本文只介绍了简单的使用。希望在进一步的学习和应用中能继续与大家分享。

   
次浏览       
 
相关文章

手机软件测试用例设计实践
手机客户端UI测试分析
iPhone消息推送机制实现与探讨
Android手机开发(一)
 
相关文档

Android_UI官方设计教程
手机开发平台介绍
android拍照及上传功能
Android讲义智能手机开发
相关课程

Android高级移动应用程序
Android系统开发
Android应用开发
手机软件测试
最新活动计划
LLM大模型应用与项目构建 12-26[特惠]
QT应用开发 11-21[线上]
C++高级编程 11-27[北京]
业务建模&领域驱动设计 11-15[北京]
用户研究与用户建模 11-21[北京]
SysML和EA进行系统设计建模 11-28[北京]

android人机界面指南
Android手机开发(一)
Android手机开发(二)
Android手机开发(三)
Android手机开发(四)
iPhone消息推送机制实现探讨
手机软件测试用例设计实践
手机客户端UI测试分析
手机软件自动化测试研究报告
更多...   


Android高级移动应用程序
Android应用开发
Android系统开发
手机软件测试
嵌入式软件测试
Android软、硬、云整合


领先IT公司 android开发平台最佳实践
北京 Android开发技术进阶
某新能源领域企业 Android开发技术
某航天公司 Android、IOS应用软件开发
阿尔卡特 Linux内核驱动
艾默生 嵌入式软件架构设计
西门子 嵌入式架构设计
更多...