Day15-NSURLSession 断点续传 + 网络封装
			
			
		 
		
		
			一、NSURLSession 下载机制原理
类型回顾:
- NSURLSessionDataTask: 用于请求和响应小数据。
- NSURLSessionDownloadTask: 用于下载大文件,支持断点续传。
- NSURLSessionUploadTask: 用于上传数据。
对于断点续传,NSURLSessionDownloadTask 是唯一支持此功能的类型。
二、断点续传机制详解
1. 如何中断任务生成 Resume Data
| 12
 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(调试用):
| 12
 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 可用性(推荐封装方法中加判断):
| 12
 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. 数据结构设计
| 12
 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. 方法定义
| 12
 3
 4
 
 | - (void)startDownloadWithURL:(NSURL *)url identifier:(NSString *)identifier;- (void)pauseDownloadWithIdentifier:(NSString *)identifier;
 - (void)resumeDownloadWithIdentifier:(NSString *)identifier;
 - (void)cancelDownloadWithIdentifier:(NSString *)identifier;
 
 | 
4. DownloadManager.m 实现要点
创建下载任务
| 12
 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];
 }
 
 | 
暂停任务
| 12
 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];
 }];
 }
 
 | 
恢复任务
| 12
 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];
 }
 
 | 
代理方法处理下载完成/进度
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 
 | - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTaskdidWriteData:(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);
 
 
 }
 
 | 
| 12
 3
 4
 5
 6
 7
 8
 9
 
 | - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTaskdidFinishDownloadingToURL:(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 后能自动恢复任务
七、工具/类库推荐