iOS应用中实现大文件下载时,最让人头疼的场景之一就是应用退到后台、被用户手动杀掉或者意外崩溃之后,再次打开应用发现下载又从0%开始了。对于一个几百兆的文件来说,这几乎是灾难性的体验。事实上,iOS系统自带的URLSession体系已经为我们提供了完善的断点续传能力,关键在于开发者是否正确地保存和使用了resumeData。这篇文章就把这套机制彻底讲透。

resumeData到底是什么,它是如何产生的
当使用NSURLSessionDownloadTask进行下载时,系统并不是把数据直接缓存在内存里,而是边下载边写入沙盒tmp目录下的一个临时文件(通常以CFNetwork开头命名)。这个临时文件记录了已经下载完成的数据内容。而resumeData,本质上就是一段经过序列化的二进制数据(内部是plist结构),里面保存着恢复下载所需的全部信息:源URL、期望接收的字节范围、已下载的字节数、临时文件的路径,以及对应的请求头信息等。
resumeData主要在两种时机产生。第一种是主动取消任务时,调用cancelByProducingResumeData:方法,取消操作完成后系统会把resumeData通过block回调给你。第二种是下载过程中发生错误时,delegate的didCompleteWithError回调中的NSError对象上,会挂着一个userinfo字典,key为NSURLSessionDownloadTaskResumeData,从中可以直接取到resumeData。这两种情况必须分开处理,很多开发者只处理了主动取消的场景,忽略了错误中断的场景,导致网络波动引起的下载中断无法恢复。
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error {
if (error) {
// 错误中断时,从error中提取resumeData
NSData *resumeData = error.userInfo[NSURLSessionDownloadTaskResumeData];
if (resumeData) {
[self saveResumeDataToFile:resumeData];
}
} else {
// 下载成功,清理本地保存的resumeData
[self deleteSavedResumeData];
}
}
- (void)pauseDownload {
if (self.downloadTask.state == NSURLSessionTaskStateRunning) {
__weak typeof(self) weakSelf = self;
[self.downloadTask cancelByProducingResumeData:^(NSData *resumeData) {
// 主动取消时通过block回调拿到resumeData
[weakSelf saveResumeDataToFile:resumeData];
}];
}
}需要注意的是,resumeData中记录的临时文件路径指向tmp目录,而系统在某些情况下会清理tmp目录。如果resumeData对应的临时文件已经被删除,用它去恢复下载就会直接失败。所以如果对可靠性要求较高,可以在拿到resumeData后,把其中引用的临时文件移动到Documents目录下进行保护,再修改resumeData中的路径信息(resumeData本质是plist,可以通过NSPropertyListSerialization解析后修改再序列化回去,不过这是比较hack的做法,官方并不保证兼容性,谨慎使用)。
持久化保存resumeData的正确姿势
拿到resumeData之后的第一件事就是持久化保存,否则应用一旦重启数据就没了。保存方案有几种,各有取舍。最简单的是NSUserDefaults,直接[[NSUserDefaults standardUserDefaults] setObject:resumeData forKey:@"resumeData"]即可。但NSUserDefaults本质是一个plist文件,整段加载进内存,如果resumeData较大或者保存的内容多,会拖慢启动速度,而且它不适合存储可能达到几十兆的二进制数据。更稳妥的方案是写入沙盒文件,把resumeData保存到Documents或Library/Caches目录下的一个独立文件中。
选择存储目录也有讲究。Documents目录会被iCloud备份,如果resumeData体积大且不需要备份,应该放在Library/Caches目录下,并用isExcludedFromBackup标记排除。Caches目录的特点是系统在磁盘空间紧张时可能清理它,这其实反而符合resumeData的语义——数据丢了最多重新下载,不影响应用功能。下面是完整的保存与读取实现。
- (NSString *)resumeDataFilePath {
NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
return [caches stringByAppendingPathComponent:@"download.resume"];
}
- (void)saveResumeDataToFile:(NSData *)resumeData {
[resumeData writeToFile:[self resumeDataFilePath] atomically:YES];
}
- (NSData *)loadSavedResumeData {
NSString *path = [self resumeDataFilePath];
if (![[NSFileManager defaultManager] fileExistsAtPath:path]) {
return nil;
}
NSData *data = [NSData dataWithContentsOfFile:path];
// 校验resumeData是否有效,无效则删除避免后续恢复失败
if (!data || ![self validateResumeData:data]) {
[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
return nil;
}
return data;
}
- (BOOL)validateResumeData:(NSData *)resumeData {
NSError *error = nil;
id plist = [NSPropertyListSerialization propertyListWithData:resumeData
options:NSPropertyListImmutable
format:nil
error:&error];
if (error || ![plist isKindOfClass:[NSDictionary class]]) {
return NO;
}
// 检查resumeData中记录的临时文件是否还存在
NSString *tempPath = plist[@"NSURLSessionResumeInfoTempFileName"];
if (tempPath && tempPath.length > 0) {
NSString *fullPath = [NSTemporaryDirectory() stringByAppendingPathComponent:tempPath];
return [[NSFileManager defaultManager] fileExistsAtPath:fullPath];
}
return YES;
}另外还有一个容易踩的坑:resumeData是与具体的URLSession配置关联的。恢复下载时必须使用与原始任务相同的session identifier创建NSURLSession,即调用[NSURLSession sessionWithConfiguration:config identifier:identifier delegate:delegate delegateQueue:nil],否则恢复可能失败或行为异常。如果应用启动时需要恢复多个下载任务,要为每个任务维护独立的标识和resumeData文件,或者统一用一个session管理所有下载。
恢复下载的完整流程与失败兜底
应用重新启动后,恢复下载的入口是[session downloadTaskWithResumeData:resumeData]。它会读取resumeData中的信息,向服务器发送携带Range请求头的GET请求,从上次中断的字节偏移处继续拉取剩余数据。服务器需要支持Range请求(返回206 Partial Content),绝大多数文件服务器和CDN都支持这一点,但如果目标服务器不支持,resumeData恢复会直接失败,此时只能重新发起完整下载。
调用downloadTaskWithResumeData之后,同样会触发delegate的didResume回调告知从哪个字节偏移继续,随后的进度回调与普通下载一致。但恢复并不总是成功的,必须做好兜底逻辑。常见的失败原因包括:resumeData损坏或为空、临时文件已被系统清理、服务器不支持断点、文件在服务器端已被更新导致Range请求返回416或200(完整内容)。正确的处理思路是在didCompleteWithError中检测错误,如果恢复失败就丢弃resumeData,从头开始一个新的下载任务,保证用户至少能得到一个可用结果而不是卡死状态。
- (void)startOrResumeDownload {
NSData *resumeData = [self loadSavedResumeData];
if (resumeData) {
// 尝试用已保存的resumeData恢复下载
self.downloadTask = [self.session downloadTaskWithResumeData:resumeData];
} else {
// 没有可用的resumeData,从头开始新任务
NSURLRequest *request = [NSURLRequest requestWithURL:self.fileURL];
self.downloadTask = [self.session downloadTaskWithRequest:request];
}
[self.downloadTask resume];
}
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error {
if (!error) {
[self handleDownloadFinished];
return;
}
NSData *resumeData = error.userInfo[NSURLSessionDownloadTaskResumeData];
if (resumeData) {
// 中断但产生了新的resumeData,覆盖保存供下次恢复
[self saveResumeDataToFile:resumeData];
} else if (self.wasResumedTask) {
// 恢复下载失败且拿不到新resumeData,丢弃旧数据重新下载
[self deleteSavedResumeData];
self.wasResumedTask = NO;
[self startFreshDownload];
}
}关于后台下载还有一点补充:如果希望应用退到后台后下载仍然继续,需要使用后台session,即把configuration的URLSessionConfiguration backgroundSessionConfigurationWithIdentifier:创建配置,并将isDiscretionary按需设置、sessionSendsLaunchEvents设为YES。后台session的特点是任务由系统进程接管,即使应用被挂起甚至被杀死,下载依然在系统层面继续,完成或失败时系统会唤醒应用并重新创建同identifier的session来交付结果。这种模式下resumeData的持久化依然有必要,因为应用被杀后delegate的内存状态全部丢失,重启后仍然要靠保存的resumeData和文件记录来重建下载队列的上下文。
总结来说,可靠的断点续传需要做好四件事:在主动取消和错误中断两个时机都正确获取resumeData;用沙盒文件而非NSUserDefaults持久化保存;恢复前校验resumeData及其临时文件的有效性;为恢复失败准备从零重下的兜底逻辑。把这四点串成完整链路,就能彻底解决下载进度丢失的问题,给用户流畅的下载体验。
NSURLSessionDownloadTaskResumeData断点续传修改时间:2026-09-04 22:25:54