在 iOS 上,断点续传主要可以用来解决"大文件下载过程中网络中断、App 退到后台或被系统挂起后,不需要重新下载"的问题。
常见可以做这些事情:
1. 大文件下载
例如:
- 视频
- 音频
- 离线地图
- 游戏资源包
- 课程资料
- 安装包或更新包
下载过程中,先把内容保存到临时文件。如果下载中断,下次继续从已有文件的位置下载。
2. App 进入后台后继续下载
iOS 可以使用后台 URLSession:
php
Swift
插入
复制
let configuration = URLSessionConfiguration.background(
withIdentifier: "com.example.file-download"
)
let session = URLSession(
configuration: configuration,
delegate: self,
delegateQueue: nil
)
然后创建下载任务:
vbnet
Swift
插入
复制
let task = session.downloadTask(with: url)
task.resume()
即使 App 进入后台,系统也可能继续执行下载。下载完成后,系统会唤醒 App,并通过代理回调通知结果。
需要注意:
后台下载由系统调度,不保证完全按照应用自己的时间执行。
系统会根据电量、网络、系统资源等情况决定是否继续。
3. 下载中断后恢复
使用 URLSessionDownloadTask 时,可以通过 resumeData 恢复任务。
下载失败时保存恢复数据:
swift
Swift
插入
复制
func urlSession(
_ session: URLSession,
task: URLSessionTask,
didCompleteWithError error: Error?
) {
guard let error = error else { return }
let userInfo = (error as NSError).userInfo
if let resumeData = userInfo[
NSURLSessionDownloadTaskResumeData
] as? Data {
// 保存 resumeData,稍后恢复
}
}
恢复时:
ini
Swift
插入
复制
let newTask = session.downloadTask(withResumeData: resumeData)
newTask.resume()
这种方式适合由 URLSession 管理下载状态,开发成本比较低。
不过 resumeData 不是绝对可靠的,不能简单认为它永远都能恢复。服务器文件变化、下载地址失效、系统版本差异等情况,都可能导致恢复失败。
因此生产环境一般需要准备重新下载的兜底逻辑。
4. 显示下载进度
可以通过代理实时获取进度:
swift
Swift
插入
复制
func urlSession(
_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64
) {
let progress = Double(totalBytesWritten) /
Double(totalBytesExpectedToWrite)
DispatchQueue.main.async {
// 更新进度条
}
}
可以展示:
- 已下载大小
- 文件总大小
- 百分比
- 下载速度
- 剩余时间
- 暂停、继续、取消按钮
例如:
arduino
text
插入
复制
已下载 320 MB / 1 GB
进度 32%
速度 5.2 MB/s
5. 暂停和继续下载
用户点击暂停时,可以取消任务并获取恢复数据:
csharp
Swift
插入
复制
downloadTask.cancel { resumeData in
if let resumeData = resumeData {
// 保存 resumeData
}
}
之后通过 downloadTask(withResumeData:) 继续。
这就能实现:
arduino
text
插入
复制
下载中 → 暂停 → 继续
但要区分:
suspend():暂时挂起任务cancel(byProducingResumeData:):取消任务,同时生成可恢复数据
如果希望应用重启后也能继续,通常需要把恢复信息持久化保存,而不是只放在内存里。
6. App 被系统终止后恢复下载
后台 URLSession 可以让系统接管下载任务。即使 App 被系统终止,系统仍可能继续执行任务。
App 下次启动时,需要使用相同的后台 Session Identifier 重新创建 Session:
vbscript
Swift
插入
复制
let configuration = URLSessionConfiguration.background(
withIdentifier: "com.example.file-download"
)
并重新设置代理,接收系统积累的下载回调。
AppDelegate 中通常还需要处理后台 Session 事件:
less
Swift
插入
复制
func application(
_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void
) {
// 保存 completionHandler
// 等 URLSession 回调处理完成后再调用
}
下载事件处理完后:
scss
Swift
插入
复制
completionHandler()
否则系统可能认为后台任务没有正确完成。
7. 自己实现 HTTP Range 断点续传
如果业务需要更强的控制,也可以自己使用 HTTP Range 请求。
例如,本地已经有 800 MB:
less
Swift
插入
复制
var request = URLRequest(url: url)
request.setValue("bytes=800000000-", forHTTPHeaderField: "Range")
服务器返回:
css
http
插入
复制
206 Partial Content
然后把响应内容追加到本地文件末尾。
这种方式可以自己控制:
- 每次从哪个字节开始
- 文件分块大小
- 失败重试
- 多线程并发下载
- 每个分片的状态
- 文件校验
- 下载任务持久化
但需要自己处理很多细节,包括:
- 文件是否发生变化
- 服务器是否支持
Range - 返回的是
200还是206 - 网络重试
- 文件追加写入
- App 重启后的状态恢复
- 下载完成后的校验和合并
通常只有在系统提供的 URLSessionDownloadTask 不够用时,才建议自己实现。
8. 多线程分片下载
对于特别大的文件,可以把文件分成多个区间并发下载:
ini
text
插入
复制
任务 1:bytes=0-999999
任务 2:bytes=1000000-1999999
任务 3:bytes=2000000-2999999
任务 4:bytes=3000000-3999999
每个任务单独保存进度:
csharp
Swift
插入
复制
struct DownloadPart {
let start: Int64
let end: Int64
var downloaded: Int64
var isFinished: Bool
}
全部下载完成后,再按顺序合并成完整文件。
这种方式可能提高速度,但也会增加:
- 内存和磁盘管理成本
- 并发任务管理成本
- 文件合并成本
- 服务端压力
- 网络切换处理难度
所以不要一开始就默认使用多线程分片。普通大文件下载,优先考虑后台 URLSession。
9. 下载完成后的文件处理
URLSession 下载完成后,系统提供的临时文件位置不一定永久有效,需要尽快移动到自己的沙盒目录。
swift
Swift
插入
复制
func urlSession(
_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL
) {
let fileManager = FileManager.default
let targetURL = ...
try? fileManager.moveItem(
at: location,
to: targetURL
)
}
通常可以保存到:
Library/Caches:可重新下载的缓存文件Library/Application Support:应用业务数据Documents:需要用户可见或长期保存的文件
如果文件只是缓存,通常优先考虑 Library/Caches。系统在磁盘空间紧张时可能清理这里的内容。
10. 做完整性校验
断点续传最后不能只看"文件大小对不对",最好还要校验文件内容。
常见方式:
- MD5
- SHA-256
- 服务端返回的文件哈希
- 文件签名校验
例如服务端告诉客户端:
arduino
text
插入
复制
文件大小:100 MB
SHA-256:xxxxxx
客户端下载完成后计算本地文件的 SHA-256:
arduino
text
插入
复制
本地计算结果 == 服务端结果
相等才认为下载成功。
否则可能存在:
- 文件下载不完整
- 分片顺序错误
- 服务端返回了错误内容
- 本地文件被破坏
- 下载地址返回了错误页面
iOS 中通常怎么选
可以简单按需求选择:
| 场景 | 推荐方案 |
|---|---|
| 普通小文件 | URLSession.dataTask |
| 普通大文件 | URLSession.downloadTask |
| 需要后台下载 | 后台 URLSession |
| 需要暂停和继续 | URLSessionDownloadTask + resumeData |
| App 重启后恢复 | 持久化任务信息 + 后台 URLSession |
| 需要精确控制字节区间 | 自定义 HTTP Range |
| 需要多线程提速 | Range 分片下载 |
| 对文件安全性要求高 | 下载完成后做哈希或签名校验 |
一个比较实用的业务设计
实际项目中可以设计一个下载管理器:
arduino
text
插入
复制
DownloadManager
├── 创建下载任务
├── 暂停下载
├── 继续下载
├── 取消下载
├── 查询下载进度
├── 持久化任务状态
├── 处理后台回调
├── 处理网络变化
├── 保存临时文件
├── 校验文件完整性
└── 移动为最终文件
任务状态可以定义为:
arduino
Swift
插入
复制
enum DownloadState {
case waiting
case downloading
case paused
case completed
case failed
case canceled
}
整体流程就是:
arduino
text
插入
复制
创建任务
↓
开始下载
↓
保存临时文件和进度
↓
网络中断 / App 进入后台
↓
系统或业务层保存任务状态
↓
恢复网络或重新启动 App
↓
继续下载剩余内容
↓
校验文件
↓
移动到最终目录
一句话概括:
iOS 做断点续传,最常见的方式是使用后台
URLSession和URLSessionDownloadTask,让系统负责下载和恢复;如果需要多线程、分片、精确控制或特殊校验,再自己基于 HTTPRange实现。