iOS开发之HTTP断点续传
前言
在APP中经常会遇到文件下载,鉴于用户体验和流量控制,就需要用到断点续传。本文主要对断点续传进行了多线程封装。
效果图
原理
HTTP实现断点续传是通过HTTP报文头部header里面设置的两个参数Range和Content-Range实现。
代码部分
一、文件大小记录
在下载文件的时候,需要先获取到文件的总大小,这里使用URL作为Key,对文件属性进行扩展的方式保存文件总大小
extension URL {
/// Get extended attribute.
func extendedAttribute(forName name: String) throws -> Data {
let data = try withUnsafeFileSystemRepresentation { fileSystemPath -> Data in
// Determine attribute size:
let length = getxattr(fileSystemPath, name, nil, 0, 0, 0)
guard length >= 0 else { throw URL.posixError(errno) }
// Create buffer with required size:
var data = Data(count: length)
// Retrieve attribute:
let result = data.withUnsafeMutableBytes { [count = data.count] in
getxattr(fileSystemPath, name, $0.baseAddress, count, 0, 0)
}
guard result >= 0 else { throw URL.posixError(errno) }
return data
}
return data
}
/// Set extended attribute.
func setExtendedAttribute(data: Data, forName name: String) throws {
try withUnsafeFileSystemRepresentation { fileSystemPath in
let result = data.withUnsafeBytes {
setxattr(fileSystemPath, name, $0.baseAddress, data.count, 0, 0)
}
guard result >= 0 else { throw URL.posixError(errno) }
}
}
/// Remove extended attribute.
func removeExtendedAttribute(forName name: String) throws {
try withUnsafeFileSystemRepresentation { fileSystemPath in
let result = removexattr(fileSystemPath, name, 0)
guard result >= 0 else { throw URL.posixError(errno) }
}
}
/// Get list of all extended attributes.
func listExtendedAttributes() throws -> [String] {
let list = try withUnsafeFileSystemRepresentation { fileSystemPath -> [String] in
let length = listxattr(fileSystemPath, nil, 0, 0)
guard length >= 0 else { throw URL.posixError(errno) }
// Create buffer with required size:
var namebuf = Array<CChar>(repeating: 0, count: length)
// Retrieve attribute list:
let result = listxattr(fileSystemPath, &namebuf, namebuf.count, 0)
guard result >= 0 else { throw URL.posixError(errno) }
// Extract attribute names:
let list = namebuf.split(separator: 0).compactMap {
$0.withUnsafeBufferPointer {
$0.withMemoryRebound(to: UInt8.self) {
String(bytes: $0, encoding: .utf8)
}
}
}
return list
}
return list
}
/// Helper function to create an NSError from a Unix errno.
private static func posixError(_ err: Int32) -> NSError {
return NSError(domain: NSPOSIXErrorDomain, code: Int(err), userInfo: [NSLocalizedDescriptionKey: String(cString: strerror(err))])
}
}
二、URLSessionDataTask下载文件
URLSessionDataTask下载文件不支持后台下载,为了方便自定义,这里使用代理的方式来实现,主要使用到的几个代理如下
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
}
三、自定义Operation
关于如何自定义请参考NSOperation的进阶使用和简单探讨,这里将下载全部封装到内部处理
class CLBreakPointResumeOperation: Operation {
var progressBlock: ((CGFloat) -> ())?
private (set) var error: CLBreakPointResumeManager.DownloadError?
private var url: URL!
private var path: String!
private var currentBytes: Int64 = 0
private var session: URLSession!
private var task: URLSessionDataTask!
private var outputStream: OutputStream?
private var taskFinished: Bool = true {
willSet {
if taskFinished != newValue {
willChangeValue(forKey: "isFinished")
}
}
didSet {
if taskFinished != oldValue {
didChangeValue(forKey: "isFinished")
}
}
}
private var taskExecuting: Bool = false {
willSet {
if taskExecuting != newValue {
willChangeValue(forKey: "isExecuting")
}
}
didSet {
if taskExecuting != oldValue {
didChangeValue(forKey: "isExecuting")
}
}
}
override var isFinished: Bool {
return taskFinished
}
override var isExecuting: Bool {
return taskExecuting
}
override var isAsynchronous: Bool {
return true
}
init(url: URL, path: String, currentBytes: Int64) {
super.init()
self.url = url
self.path = path
self.currentBytes = currentBytes
var request = URLRequest(url: url)
request.timeoutInterval = 5
if currentBytes > 0 {
let requestRange = String(format: "bytes=%llu-", currentBytes)
request.addValue(requestRange, forHTTPHeaderField: "Range")
}
session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
task = session.dataTask(with: request)
}
deinit {
print("CLBreakPointResumeOperation deinit")
}
}
extension CLBreakPointResumeOperation {
override func start() {
autoreleasepool {
if isCancelled {
taskFinished = true
taskExecuting = false
}else {
taskFinished = false
taskExecuting = true
startTask()
}
}
}
override func cancel() {
if (isExecuting) {
task.cancel()
}
super.cancel()
}
}
private extension CLBreakPointResumeOperation {
func startTask() {
task.resume()
}
func complete(_ error: CLBreakPointResumeManager.DownloadError? = nil) {
self.error = error
outputStream?.close()
outputStream = nil
taskFinished = true
taskExecuting = false
}
}
extension CLBreakPointResumeOperation: URLSessionDataDelegate {
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
if !isCancelled {
guard let response = dataTask.response as? HTTPURLResponse else {
complete(.notHTTPURLResponse)
return
}
guard response.statusCode == 200 || response.statusCode == 206 else {
complete(.statusCode(response.statusCode))
return
}
if response.statusCode == 200,
FileManager.default.fileExists(atPath: path) {
do {
try FileManager.default.removeItem(atPath: path)
currentBytes = 0
} catch {
complete(.throws(error))
return
}
}
outputStream = OutputStream(url: URL(fileURLWithPath: path), append: true)
outputStream?.open()
if currentBytes == 0 {
var totalBytes = response.expectedContentLength
let data = Data(bytes: &totalBytes, count: MemoryLayout.size(ofValue: totalBytes))
do {
try URL(fileURLWithPath: path).setExtendedAttribute(data: data, forName: "totalBytes")
} catch {
complete(.throws(error))
return
}
}
completionHandler(.allow)
}
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
session.invalidateAndCancel()
guard let response = task.response as? HTTPURLResponse else {
complete(.notHTTPURLResponse)
return
}
if let error = error {
complete(.download(error))
}else if (response.statusCode == 200 || response.statusCode == 206) {
complete()
}else {
complete(.statusCode(response.statusCode))
}
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
if !isCancelled {
let receiveBytes = dataTask.countOfBytesReceived + currentBytes
let allBytes = dataTask.countOfBytesExpectedToReceive + currentBytes
let currentProgress = min(max(0, CGFloat(receiveBytes) / CGFloat(allBytes)), 1)
DispatchQueue.main.async {
self.progressBlock?(currentProgress)
}
outputStream?.write(Array(data), maxLength: data.count)
}
}
}
四、Operation管理
使用单例持有一个字典,URL作为Key,Operation作为Value来对所有的Operation进行管理
class CLBreakPointResumeManager: NSObject {
static let shared: CLBreakPointResumeManager = CLBreakPointResumeManager()
static let folderPath: String = NSHomeDirectory() + "/Documents/CLBreakPointResume/"
private var operationDictionary = [String : CLBreakPointResumeOperation]()
private lazy var queue: OperationQueue = {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 3
return queue
}()
private lazy var operationSemap: DispatchSemaphore = {
let semap = DispatchSemaphore(value: 0)
semap.signal()
return semap
}()
private override init() {
super.init()
if !FileManager.default.fileExists(atPath: CLBreakPointResumeManager.folderPath) {
try? FileManager.default.createDirectory(atPath: CLBreakPointResumeManager.folderPath, withIntermediateDirectories: true)
}
}
}
extension CLBreakPointResumeManager {
static func download(_ url: URL, progressBlock: ((CGFloat) -> ())? = nil, completionBlock: ((Result<String, DownloadError>) -> ())? = nil) {
let completion = { result in
DispatchQueue.main.async {
completionBlock?(result)
}
}
guard operation(url.absoluteString) == nil else {
completion(.failure(.downloading))
return
}
let fileAttribute = fileAttribute(url)
guard !isDownloaded(url).0 else {
progressBlock?(1)
completion(.success(fileAttribute.path))
return
}
let operation = CLBreakPointResumeOperation(url: url, path: fileAttribute.path, currentBytes: fileAttribute.currentBytes)
operation.progressBlock = progressBlock
operation.completionBlock = {
if let error = operation.error {
completion(.failure(error))
}else {
completion(.success(fileAttribute.path))
}
removeValue(url.absoluteString)
}
shared.queue.addOperation(operation)
setOperation(operation, for: url.absoluteString)
}
static func cancel(_ url: URL) {
guard let operation = operation(url.absoluteString),
!operation.isCancelled
else {
return
}
operation.cancel()
}
static func delete(_ url: URL) throws {
cancel(url)
try FileManager.default.removeItem(atPath: filePath(url))
}
static func deleteAll() throws {
for operation in shared.operationDictionary.values where !operation.isCancelled {
operation.cancel()
}
try FileManager.default.removeItem(atPath: folderPath)
}
}
private extension CLBreakPointResumeManager {
static func operation(_ value: String) -> CLBreakPointResumeOperation? {
shared.operationSemap.wait()
let operation = shared.operationDictionary[value]
shared.operationSemap.signal()
return operation
}
static func setOperation(_ value: CLBreakPointResumeOperation, for key: String) {
shared.operationSemap.wait()
shared.operationDictionary[key] = value
shared.operationSemap.signal()
}
static func removeValue(_ value: String) {
shared.operationSemap.wait()
shared.operationDictionary.removeValue(forKey: value)
shared.operationSemap.signal()
}
}
extension CLBreakPointResumeManager {
static func isDownloaded(_ url: URL) -> (Bool, String) {
let fileAttribute = fileAttribute(url)
return (fileAttribute.currentBytes != 0 && fileAttribute.currentBytes == fileAttribute.totalBytes, fileAttribute.path)
}
}
extension CLBreakPointResumeManager {
static func fileAttribute(_ url: URL) -> (path: String, currentBytes: Int64, totalBytes: Int64) {
return (filePath(url), fileCurrentBytes(url), fileTotalBytes(url))
}
static func filePath(_ url: URL) -> String {
return folderPath + url.absoluteString.md5() + (url.pathExtension.isEmpty ? "" : ".\(url.pathExtension)")
}
static func fileCurrentBytes(_ url: URL) -> Int64 {
let path = filePath(url)
var downloadedBytes: Int64 = 0
let fileManager = FileManager.default
if fileManager.fileExists(atPath: path) {
let fileDict = try? fileManager.attributesOfItem(atPath: path)
downloadedBytes = fileDict?[.size] as? Int64 ?? 0
}
return downloadedBytes
}
static func fileTotalBytes(_ url: URL) -> Int64 {
var totalBytes : Int64 = 0
if let sizeData = try? URL(fileURLWithPath: filePath(url)).extendedAttribute(forName: "totalBytes") {
(sizeData as NSData).getBytes(&totalBytes, length: sizeData.count)
}
return totalBytes
}
}
总结
主要代码已经贴出,其中更多细节请参考详细代码,下载地址----->>>CLDemo,如果对你有所帮助,欢迎Star。
iOS开发之HTTP断点续传的更多相关文章
- iOS开发之Socket通信实战--Request请求数据包编码模块
实际上在iOS很多应用开发中,大部分用的网络通信都是http/https协议,除非有特殊的需求会用到Socket网络协议进行网络数 据传输,这时候在iOS客户端就需要很好的第三方CocoaAsyncS ...
- iOS开发之UISearchBar初探
iOS开发之UISearchBar初探 UISearchBar也是iOS开发常用控件之一,点进去看看里面的属性barStyle.text.placeholder等等.但是这些属性显然不足矣满足我们的开 ...
- iOS开发之UIImage等比缩放
iOS开发之UIImage等比缩放 评论功能真不错 评论开通后,果然有很多人吐槽.谢谢大家的支持和关爱,如果有做的不到的地方,还请海涵.毕竟我一个人的力量是有限的,我会尽自己最大的努力大家准备一些干货 ...
- iOS开发之 Xcode6 添加xib文件,去掉storyboard的hello world应用
iOS开发之 Xcode6.1创建仅xib文件,无storyboard的hello world应用 由于Xcode6之后,默认创建storyboard而非xib文件,而作为初学,了解xib的加载原理 ...
- iOS开发之loadView、viewDidLoad及viewDidUnload的关系
iOS开发之loadView.viewDidLoad及viewDidUnload的关系 iOS开发之loadView.viewDidLoad及viewDidUnload的关系 标题中所说的3个方 ...
- iOS开发之info.pist文件和.pch文件
iOS开发之info.pist文件和.pch文件 如果你是iOS开发初学者,不用过多的关注项目中各个文件的作用.因为iOS开发的学习路线起点不在这里,这些文件只会给你学习带来困扰. 打开一个项目,我们 ...
- iOS开发之WKWebView简单使用
iOS开发之WKWebView简单使用 iOS开发之 WKWebVeiw使用 想用UIWebVeiw做的,但是突然想起来在iOS8中出了一个新的WKWebView,算是UIWebVeiw的升级版. ...
- iOS 开发之Block
iOS 开发之Block 一:什么是Block.Block的作用 UI开发和网络常见功能的实现回调,按钮事件的处理方法是回调方法. 1. 按钮事件 target action 机制. 它是将一 ...
- iOS开发之Xcode常用调试技巧总结
转载自:iOS开发之Xcode常用调试技巧总结 最近在面试,面试过程中问到了一些Xcode常用的调试技巧问题.平常开发过程中用的还挺顺手的,但你要突然让我说,确实一脸懵逼.Debug的技巧很多,比如最 ...
随机推荐
- JAVA并发(8)-ThreadPoolExecutor的讲解
很久前(2020-10-23),就有想法学习线程池并输出博客,但是写着写着感觉看不懂了,就不了了之了.现在重拾起,重新写一下(学习一下). 线程池的优点也是老生常谈的东西了 减少线程创建的开销(任务数 ...
- Raspberry Pi:树莓派安装Kali2021新版本
准备材料 树莓派4B kali系统镜像 SDFormatter (格式化工具) Win32DiskImager (镜像拷录工具) 镜像下载 kali下载地址:https://www.offensive ...
- 微信app支付,完整流程,完整代码 (转)
微信app支付流程 需要的配置参数 private function wechat($body,$indent_id,$cou,$user_id,$total_fee,$ip,$domain,$non ...
- 网站图片无缝兼容 WebP/AVIF
前言 WebP 格式发布已有十余年,但不少站点至今仍未使用,只为兼顾极少数低版本浏览器.至于去年发布的 AVIF 格式,使用的站点就更少了. 然而图片往往是流量大户,与其费尽心机优化脚本体积,可能还不 ...
- ROS2学习之旅(21)——创建一个动作服务和客户节点(C++)
动作是ROS中的一种异步通信形式,动作客户端向动作服务器发送目标请求,目标服务器向操作客户端发送目标反馈和结果.本文基于前一篇自定义动作博文. 1.创建一个action_turtorials_cpp包 ...
- Apache Superset 1.2.0教程 (三)—— 图表功能详解
通过之前章节的学习,我们已经成功地安装了superset,并且连接mysql数据库,可视化了王者英雄的数据.使用的是最简单Table类型的图表,但是superset还支持非常多的图表类型. 本文我们将 ...
- 求数组的子数组之和的最大值III(循环数组)
新的要求:一维数组改成循环数组,只是涉及简单算法,只是拿了小数做测试 想法:从文件读取数组,然后新建数组,将文件读取的数组在新数组中做一下连接,成为二倍长度的数组,然后再遍历,将每次遍历的子数组的和存 ...
- vscode搭建python环境
这两天刚下了一个pycharm,结果使用之后将vscode给崩了,重装的时候有些步骤也记不清,走了一些弯路,做个总结来记录一下(本人觉得vscode比pycharm好用一点). Python下载安装 ...
- LeetCode 887. Super Egg Drop
题目链接:https://leetcode.com/problems/super-egg-drop/ 题意:给你K个鸡蛋以及一栋N层楼的建筑,已知存在某一个楼层F(0<=F<=N),在不高 ...
- python3.7 jack棋牌11点实例
# -*- codeing: utf-8 -*- # Project: 棋牌游戏11点 # Author: jack # Version: 2.2 # Start Time: 2021-07-24 i ...