preface

File downloads are common in apps, and for user experience and traffic control, breakpoint continuations are needed. This paper mainly encapsulates breakpoint continuation by multithreading.

rendering

The principle of

HTTP realization breakpoint continuingly is through HTTP message header inside the head set the two parameters of the Range and Content – Range.

HTTP file breakpoint continuation theory

Code section

One, file size record

When downloading a file, you need to obtain the total size of the file first. Here, the URL is used as the Key and the file attributes are extended to save the total size of the file

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))]) } }Copy the code

Second,URLSessionDataTaskThe download file

The URLSessionDataTask download file does not support background download. To facilitate customization, agents are used here. The main agents are as follows

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) {

}
Copy the code

3. CustomizationOperation

For details on how to customize, please refer to the advanced use and brief discussion of 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) } } }Copy the code

Four,Operationmanagement

Use singletons to hold a dictionary, urls as keys, and operations as values to manage all operations

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 } }Copy the code

conclusion

The main code has been posted, for more details please refer to the detailed code, download address —–>>>CLDemo, if you help, welcome Star.