iOS 应用开发中的断点续传实践总结
断点续传概述
断点续传就是从文件上次中断的地方开始重新下载或上传数据,而不是从文件开头。(本文的断点续传仅涉及下载,上传不在讨论之内)当下载大文件的时候,如果没有实现断点续传功能,那么每次出现异常或者用户主动的暂停,都会去重头下载,这样很浪费时间。所以项目中要实现大文件下载,断点续传功能就必不可少了。当然,断点续传有一种特殊的情况,就是 iOS 应用被用户 kill 掉或者应用 crash,要实现应用重启之后的断点续传。这种特殊情况是本文要解决的问题。
断点续传原理
要实现断点续传 , 服务器必须支持。目前最常见的是两种方式:FTP 和 HTTP。下面来简单介绍 HTTP 断点续传的原理。
HTTP
通过 HTTP,可以非常方便的实现断点续传。断点续传主要依赖于 HTTP 头部定义的 Range 来完成。具体 Range 的说明参见 RFC2616中 14.35.2 节,在请求某范围内的资源时,可以更有效地对大资源发出请求或从传输错误中恢复下载。有了 Range,应用可以通过 HTTP 请求曾经获取失败的资源的某一个返回或者是部分,来恢复下载该资源。当然并不是所有的服务器都支持 Range,但大多数服务器是可以的。Range 是以字节计算的,请求的时候不必给出结尾字节数,因为请求方并不一定知道资源的大小。Range 的定义如图 1 所示:
图 1. HTTP-Range
HTTP-Range
图 2 展示了 HTTP request 的头部信息:
图 2. HTTP request 例子
HTTP request 例子
在上面的例子中的“Range: bytes=1208765-”表示请求资源开头 1208765 字节之后的部分。
图 3 展示了 HTTP response 的头部信息:
图 3. HTTP response 例子
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
// 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 处理流程
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 的实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
// 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 流程
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 的实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// 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 的实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
//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 所示:
NSURLSessionDownloadTask 演示
总结
本文从断点续传概述开始,介绍了断点续传的应用背景,通过原理的描述,相信读者对断点续传有了基本的认识和理解。接着笔者介绍了通过 AFHTTPRequestOpeartion 实现的代码,并对 AFHTTPRequestOpeartion 做了简单的分析。最后笔者结合的实际需求,基于 NSURLConnection, NSURLSeesionDataTask 和 NSURLSessionDownloadtask。其实,下载的实现远不止这些内容,本文只介绍了简单的使用。希望在进一步的学习和应用中能继续与大家分享。
iOS 应用开发中的断点续传实践总结的更多相关文章
- 总结iOS开发中的断点续传那些事儿
前言 断点续传概述 断点续传就是从文件赏赐中断的地方重新开始下载或者上传数据,而不是从头文件开始.当下载大文件的时候,如果没有实现断点续传功能,那么每次出现异常或者用户主动的暂停,都会从头下载,这样很 ...
- 简单讲解iOS应用开发中的MD5加密的相关使用<转>
这篇文章主要介绍了iOS应用开发中的MD5加密的相关使用,示例代码基于传统的Objective-C,需要的朋友可以参考下 一.简单说明 1.说明 在开发应用的时候,数据的安全性至关重要,而仅仅用POS ...
- 简单讲解iOS应用开发中的MD5加密的相关使用
简单讲解iOS应用开发中的MD5加密的相关使用 作者:文顶顶 字体:[增加 减小] 类型:转载 时间:2015-12-19 我要评论 这篇文章主要介绍了iOS应用开发中的MD5加密的相关使用, ...
- iOS项目开发中的知识点与问题收集整理①(Part 一)
前言部分 注:本文并非绝对原创 大部分内容摘自 http://blog.csdn.net/hengshujiyi/article/details/20943045 文中有些方法可能已过时并不适用于现在 ...
- iOS项目开发中的知识点与问题收集整理①
前言部分 注:本文并非绝对原创 大部分内容摘自 http://blog.csdn.net/hengshujiyi/article/details/20943045 文中有些方法可能已过时并不适用于现在 ...
- IOS程序开发中-跳转到 发送短信界面 实现发短信
前言:我发现我标题取的不好,谁帮我取个承接上下文的标题?评论一下,我改 项目需求:在程序开发中,我们需要在某个程序里面发送一些短信验证(不是接收短信验证,关于短信验证,传送门:http://www.c ...
- iOS项目开发中的知识点与问题收集整理②(Part 二)
1.点击UIButton 无法产生触摸事件 如果在UIImageView中添加了一个按钮,你会发现在默认情况下这个按钮是无法被点击的,需要设置UIImageView的userInteractio ...
- Xamarin.IOS/Mac开发中遇到的问题
虚拟机中安装的mac系统无法识别iphone 今天在 Xamarin.iOS 应用的免费预配 时,进行到 5.插入要在其中部署应用的 iOS 设备. 在第8选择iphone设备时,发现iphone并没 ...
- iOS项目开发中的知识点与问题收集整理②
1.点击UIButton 无法产生触摸事件 如果在UIImageView中添加了一个按钮,你会发现在默认情况下这个按钮是无法被点击的,需要设置UIImageView的userInteractio ...
随机推荐
- Java内省
什么是内省? Java语言对bean类属性.事件的一种缺省处理方法,例如类A中有属性name,那我们可以通过getName,setName来得到其值或者设置新的值. 什么是JavaBean? Java ...
- windows官方多语言方案
编写 Win32 多语言用户界面应用程序 Windows 2000 针对全球市场制定了新的增强支持标准,提供了许多国际化功能,例如完全支持 Unicode.预设支持数百种语言以及用于从右向左语言的镜像 ...
- Java传参那些事!
刚刚学习java传参的时候很纠结,也非常的不理解!课本上的“按值传递”和“按址传递”搞的自己是一头雾水,后来写的项目多了,自然就明白了! 现在写传参几乎就是条件反射一般——“秒成”,分享当初自己为此写 ...
- linux 下使用 cmake安装mysql
原文地址:http://www.cppblog.com/issay789/archive/2013/01/05/196967.html 一.安装 m4 下载地址: http://files.w3pc. ...
- Spring配置bean的详细知识
在Spring中配置bean的一些细节.具体信息请参考下面的代码及注释 applicationContext.xml文件 <?xml version="1.0" encodi ...
- Key/Value之王Memcached初探:二、Memcached在.Net中的基本操作 - Edison Chou
一.Memcached ClientLib For .Net 首先,不得不说,许多语言都实现了连接Memcached的客户端,其中以Perl.PHP为主. 仅仅memcached网站上列出的语言就有: ...
- 根据Excel列类型获取列的值
using System.Data; using System.IO; using System.Text; using System.Web; using NPOI.SS.UserModel; us ...
- C# 等待另外一个窗体关闭,再进行主线程的代码
方法1 用Form类或其子类的showDialog方法. 比如你在form1里有一个按扭,然后你在Form1的点击事件里写上显示form2的代码: Form2 frm=new Form2(); frm ...
- SGU132 - Another Chocolate Maniac(状态压缩DP)
题目大意 给定一个N*M大小的大小的蛋糕,蛋糕的有些地方已经放置了东西,要求你在蛋糕上放入尽量少的1*2大小的巧克力,使得蛋糕不能够再放入巧克力 题解 和POJ1038恰好相反,此题是放入尽量少的巧克 ...
- Linux下用hostapd架无线AP
Published by 荒野无灯 on 2011-10-08 00:56:02 under 服务器/MySQL Tags: 路由,无线AP,hostapd 34452 views 本文将介绍在lin ...