Day15-NSURLSession 断点续传 + 网络封装
一、NSURLSession 下载机制原理
类型回顾:
NSURLSessionDataTask
: 用于请求和响应小数据。
NSURLSessionDownloadTask
: 用于下载大文件,支持断点续传。
NSURLSessionUploadTask
: 用于上传数据。
对于断点续传,NSURLSessionDownloadTask
是唯一支持此功能的类型。
二、断点续传机制详解
1. 如何中断任务生成 Resume Data
1 2 3 4
| [downloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) { [resumeData writeToFile:@"xxx.resume" atomically:YES]; }];
|
Resume Data 生成条件:
- 任务必须是 downloadTask。
- 必须使用
cancelByProducingResumeData
。
- Session 配置必须是
defaultSessionConfiguration
,不是 background
。
- 网络中断导致取消并不会自动生成 resumeData。
2. Resume Data 本质是什么?
resumeData
实际是一个带有 HTTP Range 请求头的二进制 plist
数据,内容包含:
- 请求 URL
- 上次下载的 Range
- 已下载的文件路径
- 请求 Header
- 临时文件信息等
你可以用如下代码解析 resumeData(调试用):
1 2 3
| NSError *error = nil; NSDictionary *resumeDict = [NSPropertyListSerialization propertyListWithData:resumeData options:0 format:nil error:&error]; NSLog(@"Resume Info: %@", resumeDict);
|
三、Resume Data 的问题与兼容性
iOS 10-13 常见问题:
- iOS 10-11 resumeData 会“损坏”,Apple Bug。
- iOS 13 后 Resume Data 更加严格:URL、Header 一点点改动就不能用。
- resumeData 不能跨机器或跨进程恢复。
校验 Resume Data 可用性(推荐封装方法中加判断):
1 2 3 4 5
| - (BOOL)isValidResumeData:(NSData *)data { if (data.length == 0) return NO; NSDictionary *dict = [NSPropertyListSerialization propertyListWithData:data options:0 format:nil error:nil]; return dict[@"NSURLSessionResumeInfoTempFileName"] != nil; }
|
四、网络封装架构设计(DownloadManager)
1. 设计目标
- 支持多任务同时下载
- 支持任务暂停、恢复、取消
- 下载进度可观察
- 稳定保存 resumeData
- 支持后台下载可选(非断点续传)
2. 数据结构设计
1 2 3 4
| @property (nonatomic, strong) NSMutableDictionary<NSString *, NSURLSessionDownloadTask *> *downloadTasks; @property (nonatomic, strong) NSMutableDictionary<NSString *, NSData *> *resumeDataDict; @property (nonatomic, strong) NSMutableDictionary<NSString *, NSNumber *> *progressDict; @property (nonatomic, strong) NSURLSession *session;
|
3. 方法定义
1 2 3 4
| - (void)startDownloadWithURL:(NSURL *)url identifier:(NSString *)identifier; - (void)pauseDownloadWithIdentifier:(NSString *)identifier; - (void)resumeDownloadWithIdentifier:(NSString *)identifier; - (void)cancelDownloadWithIdentifier:(NSString *)identifier;
|
4. DownloadManager.m 实现要点
创建下载任务
1 2 3 4 5 6 7
| - (void)startDownloadWithURL:(NSURL *)url identifier:(NSString *)identifier { if (self.downloadTasks[identifier]) return;
NSURLSessionDownloadTask *task = [self.session downloadTaskWithURL:url]; self.downloadTasks[identifier] = task; [task resume]; }
|
暂停任务
1 2 3 4 5 6 7 8 9 10
| - (void)pauseDownloadWithIdentifier:(NSString *)identifier { NSURLSessionDownloadTask *task = self.downloadTasks[identifier]; [task cancelByProducingResumeData:^(NSData * _Nullable resumeData) { if (resumeData) { self.resumeDataDict[identifier] = resumeData; [resumeData writeToFile:[self resumeDataPathForIdentifier:identifier] atomically:YES]; } [self.downloadTasks removeObjectForKey:identifier]; }]; }
|
恢复任务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| - (void)resumeDownloadWithIdentifier:(NSString *)identifier { NSData *resumeData = self.resumeDataDict[identifier]; if (!resumeData) { resumeData = [NSData dataWithContentsOfFile:[self resumeDataPathForIdentifier:identifier]]; }
NSURLSessionDownloadTask *task = nil; if ([self isValidResumeData:resumeData]) { task = [self.session downloadTaskWithResumeData:resumeData]; } else { NSURL *url = [self downloadURLForIdentifier:identifier]; task = [self.session downloadTaskWithURL:url]; }
self.downloadTasks[identifier] = task; [task resume]; }
|
代理方法处理下载完成/进度
1 2 3 4 5 6 7 8 9 10 11
| - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
NSString *identifier = [self identifierForTask:downloadTask]; CGFloat progress = (CGFloat)totalBytesWritten / totalBytesExpectedToWrite; self.progressDict[identifier] = @(progress); }
|
1 2 3 4 5 6 7 8 9
| - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location { NSString *identifier = [self identifierForTask:downloadTask]; NSURL *destinationURL = [self finalFilePathForIdentifier:identifier]; [[NSFileManager defaultManager] moveItemAtURL:location toURL:destinationURL error:nil];
[self.downloadTasks removeObjectForKey:identifier]; [self.resumeDataDict removeObjectForKey:identifier]; }
|
五、后台下载与断点续传的冲突
后台下载使用:
1
| NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"bg.download"];
|
限制:
- 不支持
cancelByProducingResumeData
,取消任务不会生成 resumeData。
- 暂停任务只能取消,不能 resume。
- 适合非断点续传的被动文件下载,比如 OS 更新、邮件附件等。
六、进阶优化建议
1. 文件分片多线程下载(适合大文件,如视频)
- 先获取文件大小(HEAD 请求)
- 分成多个 Range 任务并发下载
- 自行合并片段
2. 添加 SHA/MD5 校验
- 防止 resume 后的数据不一致
- 下载完成校验数据一致性
3. 使用数据库记录下载任务状态(可选:SQLite/FMDB)
- identifier、状态、进度、路径
- 恢复 App 后能自动恢复任务
七、工具/类库推荐