iOS 文件下载 断点续传

在 iOS 上,断点续传主要可以用来解决"大文件下载过程中网络中断、App 退到后台或被系统挂起后,不需要重新下载"的问题。

常见可以做这些事情:

1. 大文件下载

例如:

  • 视频
  • 音频
  • PDF
  • 离线地图
  • 游戏资源包
  • 课程资料
  • 安装包或更新包

下载过程中,先把内容保存到临时文件。如果下载中断,下次继续从已有文件的位置下载。

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 做断点续传,最常见的方式是使用后台 URLSessionURLSessionDownloadTask,让系统负责下载和恢复;如果需要多线程、分片、精确控制或特殊校验,再自己基于 HTTP Range 实现。

相关推荐
吴声子夜歌1 小时前
Java面试——设计模式(四)
java·设计模式·面试
yuqifang1 小时前
var deferred = jQuery.Deferred(); 使用示例
前端·javascript·面试
艾莉丝努力练剑2 小时前
【AI大模型接入SDK】C++日志体系与spdlog封装的理论部分
开发语言·c++·ide·人工智能·学习·面试·trae
吴声子夜歌11 小时前
Java面试——算法
java·算法·面试
kyriewen16 小时前
面试官让我现场用 AI 改一个真实 bug——他打断我的 3 个理由
前端·面试·ai编程
众人皆醒我独醉17 小时前
流量路由:Istio 与 Knative 的集成
面试·kubernetes·llm
众人皆醒我独醉17 小时前
模型加载:storage-initializer 与节点级缓存
面试·kubernetes·gpu
吴声子夜歌18 小时前
Java面试——设计模式(一)
java·设计模式·面试
吴声子夜歌1 天前
Java面试——设计模式(二)
java·设计模式·面试