热门标签 | HotTags
当前位置:  开发笔记 > 运维 > 正文

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

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

前言

断点续传概述

断点续传就是从文件上次中断的地方开始重新下载或上传数据,而不是从文件开头。(本文的断点续传仅涉及下载,上传不在讨论之内)当下载大文件的时候,如果没有实现断点续传功能,那么每次出现异常或者用户主动的暂停,都会去重头下载,这样很浪费时间。所以项目中要实现大文件下载,断点续传功能就必不可少了。当然,断点续传有一种特殊的情况,就是 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开发中的断点续传与实践的全部内容,其实,下载的实现远不止这些内容,本文只介绍了简单的使用。希望在进一步的学习和应用中能继续与大家分享。希望本文能帮助到有需要的大家。


推荐阅读
  • 网络运维工程师负责确保企业IT基础设施的稳定运行,保障业务连续性和数据安全。他们需要具备多种技能,包括搭建和维护网络环境、监控系统性能、处理突发事件等。本文将探讨网络运维工程师的职业前景及其平均薪酬水平。 ... [详细]
  • 在Swift项目中集成Objective-C类或第三方框架的方法
    本文通过实例讲解如何在Swift项目中引入并使用Objective-C编写的ProgressHUD库。首先需要在项目中添加库文件,并设置Objective-C桥接头文件以实现语言间的互操作性。 ... [详细]
  • 在现代网络环境中,两台计算机之间的文件传输需求日益增长。传统的FTP和SSH方式虽然有效,但其配置复杂、步骤繁琐,难以满足快速且安全的传输需求。本文将介绍一种基于Go语言开发的新一代文件传输工具——Croc,它不仅简化了操作流程,还提供了强大的加密和跨平台支持。 ... [详细]
  • 从零开始构建完整手机站:Vue CLI 3 实战指南(第一部分)
    本系列教程将引导您使用 Vue CLI 3 构建一个功能齐全的移动应用。我们将深入探讨项目中涉及的每一个知识点,并确保这些内容与实际工作中的需求紧密结合。 ... [详细]
  • 本文介绍如何通过SSH协议使用Xshell远程连接到Ubuntu系统。为了实现这一目标,需要确保Ubuntu系统已安装并配置好SSH服务器,并保证网络连通性。 ... [详细]
  • 深入解析 Spring Security 用户认证机制
    本文将详细介绍 Spring Security 中用户登录认证的核心流程,重点分析 AbstractAuthenticationProcessingFilter 和 AuthenticationManager 的工作原理。通过理解这些组件的实现,读者可以更好地掌握 Spring Security 的认证机制。 ... [详细]
  • 本文介绍如何在现有网络中部署基于Linux系统的透明防火墙(网桥模式),以实现灵活的时间段控制、流量限制等功能。通过详细的步骤和配置说明,确保内部网络的安全性和稳定性。 ... [详细]
  • 优化局域网SSH连接延迟问题的解决方案
    本文介绍了解决局域网内SSH连接到服务器时出现长时间等待问题的方法。通过调整配置和优化网络设置,可以显著缩短SSH连接的时间。 ... [详细]
  • Git管理工具SourceTree安装与使用指南
    本文详细介绍了Git管理工具SourceTree的安装、配置及团队协作方案,旨在帮助开发者更高效地进行版本控制和项目管理。 ... [详细]
  • 本文探讨了如何在Swift编程语言中先声明函数,随后再定义其具体实现的方法。 ... [详细]
  • 本文探讨了在iOS平台上开发BLE(蓝牙低功耗)应用程序时遇到的挑战,特别是如何实现应用在后台模式下仍能持续扫描并连接蓝牙设备。文章提供了具体的配置方法和常见的问题解决方案。 ... [详细]
  • 使用Swift 2.2创建我的第一个Xcode应用
    本文将指导您如何使用Xcode 6搭建并运行一个简单的iOS应用程序。从启动Xcode到执行首个应用,每个步骤都将详细介绍。 ... [详细]
  • 本文探讨了在iOS应用中实现类似Android Snack Bar功能的方法,并特别关注如何确保Snack Bar正确显示在键盘下方。 ... [详细]
  • 实现‘点击恢复’功能 - Tap-to-Resume Feature in SpriteKit
    了解如何在应用程序从非活动状态返回时,在SpriteKit游戏中添加一个‘点击恢复’的文字提示。 ... [详细]
  • 本文将探讨iOS开发过程中需要掌握的三种关键编程语言——C、Objective-C和Swift,并深入解析面向过程与面向对象编程的概念,同时对比iOS与Android两大移动平台的特点。 ... [详细]
author-avatar
mobiledu2502886333
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有