当前位置: 首页 > news >正文

断点续传Demo实现

基于我们的DownloadManager.swift代码,让我详细解释断点续传需要实现的核心功能:

断点续传的核心实现要素

1. 后台会话配置

private func setupBackgroundSession() {let config = URLSessionConfiguration.background(withIdentifier: "com.test.download")config.isDiscretionary = false        // 立即下载,不等待最佳时机config.sessionSendsLaunchEvents = true // 支持后台启动应用backgroundSession = URLSession(configuration: config, delegate: self, delegateQueue: nil)
}

作用

  • 支持应用进入后台或被杀死后继续下载
  • 系统会在下载完成后重新启动应用

2. 断点数据保存机制

private var resumeData: Data?  // 关键:保存断点续传数据// 暂停时保存断点信息
func pauseDownload() {downloadTask?.cancel { [weak self] resumeData inself?.resumeData = resumeData  // 保存断点数据self?.downloadState = .paused}
}

resumeData 包含

  • 已下载的字节数
  • 服务器响应信息
  • 临时文件路径
  • HTTP头信息

3. 智能下载启动

func startDownload(url: URL) {// 检查是否有断点数据if let resumeData = resumeData {// 从断点继续下载downloadTask = backgroundSession.downloadTask(withResumeData: resumeData)} else {// 全新下载downloadTask = backgroundSession.downloadTask(with: url)}downloadTask?.resume()
}

4. 三个关键的URLSession代理方法

进度更新
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {// bytesWritten: 本次写入的字节数// totalBytesWritten: 累计已下载字节数// totalBytesExpectedToWrite: 文件总大小downloadedSize = totalBytesWrittentotalSize = totalBytesExpectedToWritelet progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
}
断点恢复回调
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {// fileOffset: 恢复下载的起始位置// expectedTotalBytes: 文件总大小downloadedSize = fileOffset  // 更新已下载大小totalSize = expectedTotalBytes
}
下载完成处理
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {// location: 系统提供的临时文件路径// 需要将文件移动到最终位置let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]let destinationURL = documentsPath.appendingPathComponent(fileName)try FileManager.default.moveItem(at: location, to: destinationURL)downloadState = .completedresumeData = nil  // 清除断点数据
}

5. 状态管理系统

enum DownloadState {case idle        // 空闲状态case downloading // 下载中case paused      // 已暂停case completed   // 下载完成case failed      // 下载失败
}

6. 错误处理机制

func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {if let error = error {downloadState = .failed// 即使出错,resumeData仍然有效,可以继续下载}
}

断点续传的技术原理

HTTP Range 请求

当使用resumeData创建下载任务时,系统会自动发送HTTP Range请求:

GET /file.zip HTTP/1.1
Range: bytes=1048576-

表示从第1048576字节开始下载。

服务器支持要求

服务器必须支持:

  • Accept-Ranges: bytes 响应头
  • Content-Range 响应头
  • ETagLast-Modified 头(验证文件完整性)

文件完整性验证

// 实际应用中应该验证文件是否被修改
// 通过ETag或Last-Modified头进行验证

实际应用中的扩展

持久化存储

// 将resumeData保存到磁盘,应用重启后恢复
func saveResumeData() {if let resumeData = resumeData {try? resumeData.write(to: getResumeDataURL())}
}func loadResumeData() {if let data = try? Data(contentsOf: getResumeDataURL()) {resumeData = data}
}

多任务支持

// 支持多个文件同时下载
private var downloadTasks: [String: URLSessionDownloadTask] = [:]
private var resumeDataDict: [String: Data] = [:]

下载队列管理

// 控制并发下载数量
private let maxConcurrentDownloads = 3
private var downloadQueue: [URL] = []

DownloadManager实现了断点续传的所有核心功能,包括状态管理、进度跟踪、错误处理和用户控制,是一个完整的断点续传解决方案。

//
//  DownloadManager.swift
//  test
//
//  Created by Ricard.li on 2025/6/14.
//import Foundationprotocol DownloadManagerDelegate: AnyObject {func downloadProgress(_ progress: Float, totalSize: Int64, downloadedSize: Int64)func downloadCompleted(filePath: String)func downloadFailed(error: Error)func downloadPaused()func downloadResumed()
}class DownloadManager: NSObject {weak var delegate: DownloadManagerDelegate?private var downloadTask: URLSessionDownloadTask?private var backgroundSession: URLSession!private var resumeData: Data?private var downloadURL: URL?// 文件信息private var totalSize: Int64 = 0private var downloadedSize: Int64 = 0// 下载状态enum DownloadState {case idlecase downloadingcase pausedcase completedcase failed}private var downloadState: DownloadState = .idleoverride init() {super.init()setupBackgroundSession()}private func setupBackgroundSession() {let config = URLSessionConfiguration.background(withIdentifier: "com.test.download")config.isDiscretionary = falseconfig.sessionSendsLaunchEvents = truebackgroundSession = URLSession(configuration: config, delegate: self, delegateQueue: nil)}// 开始下载func startDownload(url: URL) {guard downloadState != .downloading else { return }downloadURL = urldownloadState = .downloading// 检查是否有之前的断点数据if let resumeData = resumeData {downloadTask = backgroundSession.downloadTask(withResumeData: resumeData)} else {downloadTask = backgroundSession.downloadTask(with: url)}downloadTask?.resume()delegate?.downloadResumed()}// 暂停下载func pauseDownload() {guard downloadState == .downloading else { return }downloadTask?.cancel { [weak self] resumeData inself?.resumeData = resumeDataself?.downloadState = .pausedDispatchQueue.main.async {self?.delegate?.downloadPaused()}}}// 恢复下载func resumeDownload() {guard downloadState == .paused, let resumeData = resumeData else { return }downloadState = .downloadingdownloadTask = backgroundSession.downloadTask(withResumeData: resumeData)downloadTask?.resume()delegate?.downloadResumed()}// 取消下载func cancelDownload() {downloadTask?.cancel()resumeData = nildownloadState = .idledownloadedSize = 0totalSize = 0}// 获取下载进度func getProgress() -> Float {guard totalSize > 0 else { return 0.0 }return Float(downloadedSize) / Float(totalSize)}// 获取下载状态func getCurrentState() -> DownloadState {return downloadState}// 获取文件大小信息func getSizeInfo() -> (total: Int64, downloaded: Int64) {return (totalSize, downloadedSize)}
}// MARK: - URLSessionDownloadDelegate
extension DownloadManager: URLSessionDownloadDelegate {func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {// 下载完成,移动文件到Documents目录let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]let fileName = downloadURL?.lastPathComponent ?? "downloaded_file"let destinationURL = documentsPath.appendingPathComponent(fileName)do {// 如果文件已存在,先删除if FileManager.default.fileExists(atPath: destinationURL.path) {try FileManager.default.removeItem(at: destinationURL)}try FileManager.default.moveItem(at: location, to: destinationURL)downloadState = .completedresumeData = nilDispatchQueue.main.async {self.delegate?.downloadCompleted(filePath: destinationURL.path)}} catch {downloadState = .failedDispatchQueue.main.async {self.delegate?.downloadFailed(error: error)}}}func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {// 更新下载进度downloadedSize = totalBytesWrittentotalSize = totalBytesExpectedToWritelet progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)DispatchQueue.main.async {self.delegate?.downloadProgress(progress, totalSize: totalBytesExpectedToWrite, downloadedSize: totalBytesWritten)}}func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {// 恢复下载时调用downloadedSize = fileOffsettotalSize = expectedTotalByteslet progress = Float(fileOffset) / Float(expectedTotalBytes)DispatchQueue.main.async {self.delegate?.downloadProgress(progress, totalSize: expectedTotalBytes, downloadedSize: fileOffset)}}
}// MARK: - URLSessionTaskDelegate
extension DownloadManager: URLSessionTaskDelegate {func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {if let error = error {// 检查是否是用户取消操作(包括暂停)let nsError = error as NSErrorif nsError.domain == NSURLErrorDomain && nsError.code == NSURLErrorCancelled {// 如果是取消操作,不处理为错误// 暂停操作会通过pauseDownload()方法的回调处理return}// 其他错误才标记为失败downloadState = .failedDispatchQueue.main.async {self.delegate?.downloadFailed(error: error)}}}
}// MARK: - 辅助方法
extension DownloadManager {// 格式化文件大小static func formatFileSize(_ size: Int64) -> String {let formatter = ByteCountFormatter()formatter.allowedUnits = [.useMB, .useKB, .useBytes]formatter.countStyle = .filereturn formatter.string(fromByteCount: size)}// 计算下载速度func calculateDownloadSpeed(downloadedBytes: Int64, timeInterval: TimeInterval) -> String {let speed = Double(downloadedBytes) / timeIntervallet formatter = ByteCountFormatter()formatter.allowedUnits = [.useMB, .useKB, .useBytes]formatter.countStyle = .filereturn formatter.string(fromByteCount: Int64(speed)) + "/s"}
} 
//
//  ViewController.swift
//  test
//
//  Created by Ricard.li on 2025/6/14.
//import UIKit
import SnapKitclass ViewController: UIViewController {private let downloadManager = DownloadManager()// UI组件private lazy var titleLabel: UILabel = {let label = UILabel()label.text = "断点续传演示"label.font = UIFont.boldSystemFont(ofSize: 24)label.textAlignment = .centerlabel.textColor = .blackreturn label}()private lazy var urlTextField: UITextField = {let textField = UITextField()textField.placeholder = "请输入下载链接"textField.borderStyle = .roundedRecttextField.font = UIFont.systemFont(ofSize: 16)textField.text = "https://media.w3.org/2010/05/sintel/trailer.mp4"return textField}()private lazy var progressView: UIProgressView = {let progress = UIProgressView(progressViewStyle: .default)progress.progressTintColor = .systemBlueprogress.trackTintColor = .systemGray5progress.progress = 0.0return progress}()private lazy var progressLabel: UILabel = {let label = UILabel()label.text = "0%"label.font = UIFont.systemFont(ofSize: 16)label.textAlignment = .centerlabel.textColor = .systemBluereturn label}()private lazy var statusLabel: UILabel = {let label = UILabel()label.text = "等待开始"label.font = UIFont.systemFont(ofSize: 14)label.textAlignment = .centerlabel.textColor = .systemGrayreturn label}()private lazy var sizeLabel: UILabel = {let label = UILabel()label.text = "0 MB / 0 MB"label.font = UIFont.systemFont(ofSize: 14)label.textAlignment = .centerlabel.textColor = .systemGrayreturn label}()private lazy var speedLabel: UILabel = {let label = UILabel()label.text = "下载速度: 0 KB/s"label.font = UIFont.systemFont(ofSize: 14)label.textAlignment = .centerlabel.textColor = .systemGrayreturn label}()private lazy var startButton: UIButton = {let button = UIButton(type: .system)button.setTitle("开始下载", for: .normal)button.backgroundColor = .systemBluebutton.setTitleColor(.white, for: .normal)button.layer.cornerRadius = 8button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 16)button.addTarget(self, action: #selector(startDownload), for: .touchUpInside)return button}()private lazy var pauseButton: UIButton = {let button = UIButton(type: .system)button.setTitle("暂停", for: .normal)button.backgroundColor = .systemOrangebutton.setTitleColor(.white, for: .normal)button.layer.cornerRadius = 8button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 16)button.addTarget(self, action: #selector(pauseDownload), for: .touchUpInside)button.isEnabled = falsereturn button}()private lazy var resumeButton: UIButton = {let button = UIButton(type: .system)button.setTitle("继续", for: .normal)button.backgroundColor = .systemGreenbutton.setTitleColor(.white, for: .normal)button.layer.cornerRadius = 8button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 16)button.addTarget(self, action: #selector(resumeDownload), for: .touchUpInside)button.isEnabled = falsereturn button}()private lazy var cancelButton: UIButton = {let button = UIButton(type: .system)button.setTitle("取消", for: .normal)button.backgroundColor = .systemRedbutton.setTitleColor(.white, for: .normal)button.layer.cornerRadius = 8button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 16)button.addTarget(self, action: #selector(cancelDownload), for: .touchUpInside)button.isEnabled = falsereturn button}()private lazy var buttonStackView: UIStackView = {let stackView = UIStackView(arrangedSubviews: [startButton, pauseButton, resumeButton, cancelButton])stackView.axis = .horizontalstackView.distribution = .fillEquallystackView.spacing = 12return stackView}()// 用于计算下载速度private var lastDownloadedSize: Int64 = 0private var lastUpdateTime: Date = Date()override func viewDidLoad() {super.viewDidLoad()setupUI()setupDownloadManager()}private func setupUI() {view.backgroundColor = .systemBackground// 添加所有UI组件view.addSubview(titleLabel)view.addSubview(urlTextField)view.addSubview(progressView)view.addSubview(progressLabel)view.addSubview(statusLabel)view.addSubview(sizeLabel)view.addSubview(speedLabel)view.addSubview(buttonStackView)// 设置约束titleLabel.snp.makeConstraints { make inmake.top.equalTo(view.safeAreaLayoutGuide.snp.top).offset(30)make.centerX.equalToSuperview()}urlTextField.snp.makeConstraints { make inmake.top.equalTo(titleLabel.snp.bottom).offset(30)make.leading.trailing.equalToSuperview().inset(20)make.height.equalTo(44)}progressView.snp.makeConstraints { make inmake.top.equalTo(urlTextField.snp.bottom).offset(40)make.leading.trailing.equalToSuperview().inset(20)make.height.equalTo(8)}progressLabel.snp.makeConstraints { make inmake.top.equalTo(progressView.snp.bottom).offset(12)make.centerX.equalToSuperview()}statusLabel.snp.makeConstraints { make inmake.top.equalTo(progressLabel.snp.bottom).offset(12)make.centerX.equalToSuperview()}sizeLabel.snp.makeConstraints { make inmake.top.equalTo(statusLabel.snp.bottom).offset(8)make.centerX.equalToSuperview()}speedLabel.snp.makeConstraints { make inmake.top.equalTo(sizeLabel.snp.bottom).offset(8)make.centerX.equalToSuperview()}buttonStackView.snp.makeConstraints { make inmake.top.equalTo(speedLabel.snp.bottom).offset(40)make.leading.trailing.equalToSuperview().inset(20)make.height.equalTo(50)}}private func setupDownloadManager() {downloadManager.delegate = self}@objc private func startDownload() {guard let urlString = urlTextField.text, !urlString.isEmpty,let url = URL(string: urlString) else {showAlert(title: "错误", message: "请输入有效的下载链接")return}downloadManager.startDownload(url: url)updateButtonStates(downloading: true)lastDownloadedSize = 0lastUpdateTime = Date()}@objc private func pauseDownload() {downloadManager.pauseDownload()}@objc private func resumeDownload() {downloadManager.resumeDownload()updateButtonStates(downloading: true)lastUpdateTime = Date()}@objc private func cancelDownload() {downloadManager.cancelDownload()resetUI()}private func updateButtonStates(downloading: Bool) {startButton.isEnabled = !downloadingpauseButton.isEnabled = downloadingresumeButton.isEnabled = falsecancelButton.isEnabled = downloading}private func resetUI() {progressView.progress = 0.0progressLabel.text = "0%"statusLabel.text = "等待开始"sizeLabel.text = "0 MB / 0 MB"speedLabel.text = "下载速度: 0 KB/s"startButton.isEnabled = truepauseButton.isEnabled = falseresumeButton.isEnabled = falsecancelButton.isEnabled = false}private func showAlert(title: String, message: String) {let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)alert.addAction(UIAlertAction(title: "确定", style: .default))present(alert, animated: true)}
}// MARK: - DownloadManagerDelegate
extension ViewController: DownloadManagerDelegate {func downloadProgress(_ progress: Float, totalSize: Int64, downloadedSize: Int64) {DispatchQueue.main.async {self.progressView.progress = progressself.progressLabel.text = String(format: "%.1f%%", progress * 100)let totalSizeStr = DownloadManager.formatFileSize(totalSize)let downloadedSizeStr = DownloadManager.formatFileSize(downloadedSize)self.sizeLabel.text = "\(downloadedSizeStr) / \(totalSizeStr)"// 计算下载速度let currentTime = Date()let timeInterval = currentTime.timeIntervalSince(self.lastUpdateTime)if timeInterval > 1.0 { // 每秒更新一次速度let speedBytes = downloadedSize - self.lastDownloadedSizelet speed = Double(speedBytes) / timeIntervallet speedStr = DownloadManager.formatFileSize(Int64(speed))self.speedLabel.text = "下载速度: \(speedStr)/s"self.lastDownloadedSize = downloadedSizeself.lastUpdateTime = currentTime}}}func downloadCompleted(filePath: String) {DispatchQueue.main.async {self.statusLabel.text = "下载完成"self.speedLabel.text = "下载完成"self.resetUI()self.showAlert(title: "下载完成", message: "文件已保存到: \(filePath)")}}func downloadFailed(error: Error) {DispatchQueue.main.async {self.statusLabel.text = "下载失败"self.resetUI()self.showAlert(title: "下载失败", message: error.localizedDescription)}}func downloadPaused() {DispatchQueue.main.async {self.statusLabel.text = "已暂停"self.startButton.isEnabled = falseself.pauseButton.isEnabled = falseself.resumeButton.isEnabled = trueself.cancelButton.isEnabled = true}}func downloadResumed() {DispatchQueue.main.async {self.statusLabel.text = "下载中..."self.updateButtonStates(downloading: true)}}
}
http://www.lryc.cn/news/610325.html

相关文章:

  • 【目标检测基础】——yolo学习
  • 设备电机状态监测中的故障诊断与定位策略
  • HCIP笔记1
  • 微信小程序本地存储与Cookie的区别
  • 【node】如何开发一个生成token的接口
  • DolphinScheduler 集成DataX
  • 【REACT18.x】封装react-rouer实现多级路由嵌套,封装登录态权限拦截
  • 《Python 实用项目与工具制作指南》· 2.1 输入输出
  • 基于Matlab实现LDA算法
  • 【机器学习】(算法优化一)集成学习之:装袋算法(Bagging):装袋决策树、随机森林、极端随机树
  • MiDSS复现
  • 测试-概念篇(3)
  • 基于SpringBoot的OA办公系统的设计与实现
  • 【Mac】OrbStack:桌面端虚拟机配置与使用
  • 防火墙认证用户部署
  • DPDK中的TCP头部处理
  • 在安卓中使用 FFmpegKit 剪切视频并添加文字水印
  • uiautomator2 编写测试流程-登陆后的酷狗01
  • Django集成图片验证码功能:基于django-simple-captcha实现
  • MySQL Router
  • Elasticsearch Ingest Pipeline 实现示例
  • C 语言枚举、typedef 与预处理详解
  • C语言的数组与字符串
  • AI产品经理面试宝典第61天:AI产品体验、数据安全与架构实战解析
  • 倒排索引:Elasticsearch 搜索背后的底层原理
  • 无公网环境下在centos7.9上使用kk工具部署k8s平台(amd64架构)
  • 数字信号处理_编程实例1
  • 【前端】JavaScript基础知识及基本应用
  • C++ STL list容器详解:从基础使用到高级特性
  • AI绘图-Stable Diffusion-WebUI的基本用法