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。其实,下载的实现远不止这些内容,本文只介绍了简单的使用。希望在进一步的学习和应用中能继续与大家分享。

时间: 2024-11-08 19:40:47

iOS 应用开发中的断点续传实践总结的相关文章

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

前言 断点续传概述 断点续传就是从文件赏赐中断的地方重新开始下载或者上传数据,而不是从头文件开始.当下载大文件的时候,如果没有实现断点续传功能,那么每次出现异常或者用户主动的暂停,都会从头下载,这样很浪费时间有木有.所以呢,项目中实现大文件下载的时候,断点续传功能是必不可少了.当然咯,断点续传有一种特殊的情况,就是我们的应用呗用户kill掉或者应用crash,要实现应用重启之后的断点续传,这种情况就是我们将要解决的问题. 断点续传的原理 要实现断点续传,服务器必须是要支持的.目前最常见的两种方式

SharePoint开发中的最佳实践(再续)

SharePoint开发中的最佳实践 20.SPViewScope介绍 Default :仅显示指定文件夹下的文件和子文件夹 FilesOnly:仅显示指定文件夹下的文件 Recursive:显示所有文件夹下的文件 RecursiveAll:显示所有文件夹下的所有文件和子文件夹 以上这个属性使用在CAML中,用于控制在哪些范围内进行查找,我们可以根据实际的查找需求,对这个ViewAttribute属性进行设置,来完成我们的操作. 21.SPGridView介绍 SPGridView在GridVi

iOS项目开发中的知识点与问题收集整理(Part 一)

前言部分 注:本文并非绝对原创 大部分内容摘自 http://blog.csdn.net/hengshujiyi/article/details/20943045 文中有些方法可能已过时并不适用于现在的开发环境. 1.Search Bar 怎样去掉背景的颜色(storyboard里只能设置background颜色,可是发现clear Color无法使用). 其实在代码里还是可以设置的,那就是删除背景view  [[self.searchBar.subviews objectAtIndex:0]

iOS项目开发中的知识点与问题收集整理

注:本文并非绝对原创 大部分内容摘自 http://blog.csdn.net/hengshujiyi/article/details/20943045 文中有些方法可能已过时并不适用于现在的开发环境. 1,Search Bar 怎样去掉背景的颜色(storyboard里只能设置background颜色,可是发现clear Color无法使用). 其实在代码里还是可以设置的,那就是删除背景view  [[self.searchBar.subviews objectAtIndex:0] remov

MVP模式在Android开发中的最佳实践

这篇文章拖了好久了,一直存在草稿箱里没有继续写,趁今天有空,撸撸完. 回想一下,你刚刚学习Android的时候,总会看到一些书上写着,Android使用的是MVC模式,Activity就是一个Controller,或许那个时候,你没有什么深刻的体会.随着经验的积累.你发现,Activity既是Controller,掌管着许许多多的业务逻辑,同时它也作为View的一部分,控制着视图层的显示.久而久之,这个Controller便显得过于重,职责不再那么单一. 于是,再后来,为了使Activity的职

彼之蜜糖,吾之砒霜——聊聊软件开发中的最佳实践

"描述一个事物,唯有一个名词定义它的概念,唯有一个动词揭露它的行为,唯有一个形容词表现它的特征.要做的,就是用心去寻找那个名词.那个动词.那个形容词--" -- 福楼拜 (Gustave Flaubert) 我想讲个故事. 很久很久以前(一般讲故事都是这样开头吧), 两个老工程师在一起聊天,谈各自生涯中最自豪的工程.其中一个先讲述了他的杰作: " 我们建造的桥,横跨一个峡谷,峡谷很宽很深.我们花了两年时间研究地质,选择材料.聘请了最好的工程师团队来设计方案,而这又花了五年时间

ios 界面开发中的常见元素

界面开发中的 CGPoint.CGSize.CGRect.CGRectMake.window(窗口).视图(view)简单记录 定义 /* Points. */ struct CGPoint { CGFloat x; CGFloat y; }; typedef struct CGPoint CGPoint; /* Sizes. */ struct CGSize { CGFloat width; CGFloat height; }; typedef struct CGSize CGSize; /*

Hybrid App Development: 二、关于造轮子以及在Xcode iOS应用开发中加入Cordova

转载请注明出处:http://www.cnblogs.com/xdxer/p/4111552.html [ctrl+左键点击图片可以查看原图] 在上一篇关于Hybrid App Development的文章中,我讨论了一下在iOS下UIWebView的使用方法.但是光使用一个UIWebView所提供的功能还是完全不能满足我们的需求.   关于造轮子的思考: 在UIKit中的UIWebView虽然已经提供了很多功能了,比如JavaScript和Objc之间的通信.但是考虑到一个问题,如果在Hybr

ios app 开发中ipa重新签名步骤介绍

作为一个app应用程序开发者,在app应用程序在苹果商店上架前总需要将安装包安装到ios机器上进行测试,这个时候我们就需要打包in house版本的ipa了,打包in house实际上是一个将ipa应用程序重新签名的一个过程.一般来说打包in house需要以下东西:MAC机器,一般打包ipa都是在MAC机上打包的,一个后缀名为.mobileprovision概要配置文件,一个后缀名为P12的证书,还有一个后缀名为.cer的证书,还有就是你想重新签名的ipa. 如何给ipa重新签名 步骤1 :