iOS的后台下载是系统提供的一项非常实用的能力,它允许应用在退到后台甚至被用户手动划掉之后,继续完成大文件的网络下载。但这项能力的接入并不像普通的网络请求那么直观,很多开发者按照前台请求的写法照搬到后台场景,结果发现应用一切到后台下载就停了,或者下载完成后回调根本不走,或者拿不到下载好的文件。这些问题几乎都指向同一个根源:会话配置和代理回调的使用方式不符合后台下载的规范。这篇文章从配置、代理、恢复机制三个层面,把后台下载的完整链路讲清楚。

为什么普通URLSession不能用于后台下载
iOS里的URLSession按生命周期分为三种:默认会话、临时会话和后台会话。前两种会话的生命周期和应用程序进程绑定,应用一旦挂起或被杀掉,会话连同其中所有的任务都会被系统回收,下载自然中断。而后台会话由系统的守护进程接管,即使应用进程不在了,传输任务依然在系统层面继续进行,等下载完成或需要鉴权时,系统再唤醒应用处理结果。
创建后台会话的关键是调用URLSessionConfiguration.background(withIdentifier:),传入一个全局唯一的标识符。这个标识符非常重要,系统靠它来匹配被唤醒的应用和之前的会话,恢复任务时也依赖它。下面是一个标准的创建方式:
let config = URLSessionConfiguration.background(withIdentifier: "com.myapp.downloadSession")
// 后台会话建议开启这个选项,允许系统在需要时唤醒应用
config.isDiscretionary = true
// 超时设置:后台会话的超时与前台不同,requestInterval 针对单次请求
config.timeoutIntervalForRequest = 60
// HTTP 是否允许蜂窝网络,可以按业务需求设置
config.allowsCellularAccess = true
let session = URLSession(configuration: config,
delegate: self,
delegateQueue: nil)
这里有几个容易被忽略的点。第一,同一个identifier的后台会话在同一时间只能存在一个实例,重复创建会导致系统抛出异常或者行为异常,所以通常把它做成单例。第二,isDiscretionary设为true时,系统会选择合适的时机(比如接上电源、连上Wi-Fi)再执行传输,适合不紧急的大文件,紧急任务则设为false。第三,绝对不要复用前台的shared会话去做后台下载,它不具备后台续传能力。
实现NSURLSessionDownloadDelegate的三个关键回调
后台会话必须配合代理模式使用,因为下载的进展和结果都通过代理回调通知应用,而不是通过任务闭包。使用downloadTask(with:)创建任务后,需要实现URLSessionDownloadDelegate中的方法,最重要的有三个。
extension DownloadManager: URLSessionDownloadDelegate {
// 下载过程中持续回调,可用来更新进度
func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64) {
let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
DispatchQueue.main.async {
// 更新UI进度条
}
}
// 下载完成,临时文件只在回调期间有效,必须立刻搬走
func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL) {
let target = FileManager.default
.urls(for: .documentDirectory, in: .userDomainMask)[0]
.appendingPathComponent("video.mp4")
try? FileManager.default.moveItem(at: location, to: target)
}
// 任务出错,包含取消、失败等情况
func urlSession(_ session: URLSession,
task: URLSessionTask,
didCompleteWithError error: Error?) {
if let error = error {
let nsError = error as NSError
if nsError.code == NSURLErrorCancelled {
// 被取消的任务,userInfo里带有恢复数据
if let resumeData = nsError.userInfo[NSURLSessionDownloadTaskResumeData] as? Data {
self.saveResumeData(resumeData, for: task)
}
}
}
}
}
第一个常见的坑是文件搬移。didFinishDownloadingTo给出的location指向系统临时目录里的文件,这个文件在回调返回后就会被系统删除。如果只是把URL记下来等之后再处理,文件早就没了,必须在这个回调内部立刻用FileManager把文件移动或复制到应用的沙盒目录。这也是很多开发者反馈“下载完成但找不到文件”的直接原因。
第二个常见的坑是错误回调的混淆。didCompleteWithError并不只在失败时触发,任务被取消时也会走到这里,错误码是NSURLErrorCancelled。此时错误对象的userInfo中带有NSURLSessionDownloadTaskResumeData,这是一段二进制数据,保存下来之后可以随时用它重建下载任务并从断点继续。用户主动暂停下载的场景就应该走这条路径:调用task.cancel(byProducingResumeData:),把返回的resumeData持久化,下次再通过downloadTask(withResumeData:)恢复。
应用被杀后的恢复机制与系统唤醒流程
后台下载最精华也最容易出错的部分是恢复流程。应用退到后台后,如果下载还在进行,系统会在下载完成时重新启动应用(哪怕它已经被杀掉),并通过AppDelegate的特定方法把控制权交回来。整个流程是这样的:应用进入后台时,系统生成一个completionHandler,通过application:handleEventsForBackgroundURLSession:completionHandler:交给应用保管;应用拿到这个handler后必须保存起来,并用同一个identifier重建会话,让会话重新挂上代理;下载回调全部走完之后,调用之前保存的handler通知系统收尾。
func application(_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void) {
// 只有identifier匹配时才处理,避免处理了别的会话
if identifier == "com.myapp.downloadSession" {
// 保存handler,等下载任务全部结束后再调用
BackgroundCompletionManager.shared.completionHandler = completionHandler
// 用同一个identifier重建会话,触发系统回放未处理的代理回调
_ = DownloadManager.shared.session
}
}
这里强调两点。第一,必须用getTasksWithCompletionHandler或重建会话的方式让代理回调“回放”。系统唤醒应用后不会自动触发之前积压的代理方法,只有当应用重新创建了同identifier的会话,系统才会把下载结果重新派发给代理。如果重建的identifier不一致,或者忘了设置delegate,就会出现“后台下载明明完成了但应用毫无反应”的现象。第二,completionHandler只能调用一次,调用时机是所有代理回调处理完毕之后(通常放在urlSessionDidFinishEvents里)。不调用它系统会认为应用还没处理完,界面会一直停留在重新启动的快照上;重复调用则可能直接导致崩溃。
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
DispatchQueue.main.async {
if let handler = BackgroundCompletionManager.shared.completionHandler {
BackgroundCompletionManager.shared.completionHandler = nil
handler()
}
}
}
应用启动时的主动恢复也要处理。冷启动后应调用session.getAllTasks检查是否有残留的后台任务,如果任务处于suspended状态要调用resume()让它们继续,同时把任务和业务数据重新关联起来,这样界面上才能正确显示哪些文件还在下载。如果应用是被系统杀掉而不是下载完成唤醒的,系统也可能不保证传输一定继续,iOS会根据资源情况决定,所以对关键任务要做好失败重试和断点续传的兜底逻辑。
后台下载失败的常见原因与排查思路
排查后台下载问题可以按照固定顺序来。先确认会话类型,检查是否真的用了background(withIdentifier:)创建会话,这是最常见的错误。其次检查identifier的唯一性和一致性,任何一次重建都要保证字符串完全相同。第三检查代理是否被强引用,URLSession对delegate是强引用的,反过来如果代理对象又强持有会话就会造成引用循环,会话永不释放,表现是任务状态诡异、回调紊乱。正确做法是让管理类持有session,session强持有delegate,delegate通过weak引用管理类。
还有一个高频问题是服务端不支持断点续传。resumeData恢复下载时,系统会带上Range请求头,如果服务器返回的响应里没有Accept-Ranges: bytes,或者对Range请求返回200而不是206,续传就会从头开始甚至失败。可以在Charles里抓包确认。另外,后台会话不支持uploadTask(with:fromFile:)以外的部分上传特性,也不支持自定义HTTP body stream,遇到这类需求要改用前台会话加后台续传时间的方案。逐项对照检查,绝大多数后台下载失败都能定位到具体环节并解决。
Background URLSessionNSURLSessionDownloadDelegateiOS后台下载修改时间:2026-09-12 17:32:48