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

时间:2022-08-24 10:51:36

前言

断点续传概述

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

断点续传原理

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

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

http

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

range 的定义如图 1 所示:

图 1. http-range

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

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

图 2. http request 例子

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

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

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

图 3. http response 例子

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

上面例子中的”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
40
41
42
43
44
45
46
47
48
// 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 处理流程

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

这里 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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// 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 流程

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

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

实现效果如图 6 所示:

图 6. nsurlconnection 演示

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

nsurlsessiondatatask

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

图 7. 协议之间映射关系

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

代码如清单 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
24
// 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 演示

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

nsurlsessiondowntask

最后来看下 nsurlsession 中用来下载的类 nsurlsessiondownloadtask,对应的协议是 nsurlsessiondownloaddelegate,如图 9 所示:

图 9. nsurlsessiondownloaddelegate 协议

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

其中在退出 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
41
42
43
44
//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开发中的断点续传与实践

总结

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