断点续传的原理
断点续传允许在下载中断后从中断处继续下载,其核心依赖于 HTTP协议的Range请求头:
-
Range头机制:客户端通过
Range: bytes=start-头告知服务器需要从哪个字节开始传输数据。 -
服务器响应:若服务器支持,返回
206 Partial Content状态码及对应数据块;否则返回200 OK及完整数据。 - 本地文件管理:客户端需记录已下载大小,将新数据追加到文件末尾,确保中断后能恢复。
实现方式一:使用 NSURLSessionDownloadTask 和 ResumeData
步骤说明
-
创建下载任务:初始化
NSURLSessionDownloadTask。 -
暂停并保存ResumeData:调用
cancelByProducingResumeData:获取恢复数据。 -
持久化ResumeData:将
resumeData保存到磁盘或UserDefaults。 -
恢复下载:用
resumeData重新创建任务继续下载。 - 完成处理:移动临时文件到目标路径。
代码示例
// 定义属性
@property (nonatomic, strong) NSURLSessionDownloadTask *downloadTask;
@property (nonatomic, strong) NSData *resumeData;
@property (nonatomic, strong) NSURLSession *session;
// 开始/恢复下载
- (void)startDownload {
NSURL *url = [NSURL URLWithString:@"https://example.com/largefile.zip"];
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
self.session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
if (self.resumeData) {
self.downloadTask = [self.session downloadTaskWithResumeData:self.resumeData];
} else {
self.downloadTask = [self.session downloadTaskWithURL:url];
}
[self.downloadTask resume];
}
// 暂停下载
- (void)pauseDownload {
__weak typeof(self) weakSelf = self;
[self.downloadTask cancelByProducingResumeData:^(NSData *resumeData) {
weakSelf.resumeData = resumeData;
weakSelf.downloadTask = nil;
// 保存到本地
[[NSUserDefaults standardUserDefaults] setObject:resumeData forKey:@"resumeData"];
}];
}
// 处理下载完成
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location {
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSString *destinationPath = [documentsPath stringByAppendingPathComponent:@"downloadedFile.zip"];
[fileManager removeItemAtPath:destinationPath error:nil];
[fileManager moveItemAtURL:location toURL:[NSURL fileURLWithPath:destinationPath] error:nil];
// 清除resumeData
self.resumeData = nil;
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"resumeData"];
}
// 错误处理
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
if (error) {
// 检查是否有resumeData
self.resumeData = error.userInfo[NSURLSessionDownloadTaskResumeData];
[[NSUserDefaults standardUserDefaults] setObject:self.resumeData forKey:@"resumeData"];
}
}
注意事项
- resumeData有效期:若URL或服务器资源变化可能导致失效。
-
后台会话:需使用
backgroundSessionConfiguration以支持后台下载。 - 文件存储:确保临时文件未被系统清理。
实现方式二:手动处理 Range 头(使用 NSURLSessionDataTask)
步骤说明
- 检查本地文件:获取已下载大小。
-
设置Range头:构造请求时指定
bytes=start-。 -
处理服务器响应:
-
206:追加写入数据。 -
200:重新下载(服务器不支持Range)。
-
- 实时保存进度:每次写入后更新已下载大小。
- 完成下载:移动文件并清理状态。
代码示例
// 定义属性
@property (nonatomic, strong) NSURLSessionDataTask *dataTask;
@property (nonatomic, assign) long long currentLength;
@property (nonatomic, strong) NSFileHandle *fileHandle;
@property (nonatomic, copy) NSString *tempFilePath;
- (void)startManualResumeDownload {
NSURL *url = [NSURL URLWithString:@"https://example.com/largefile.zip"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
// 获取已下载大小
self.tempFilePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"temp.zip"];
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager fileExistsAtPath:self.tempFilePath]) {
NSDictionary *attrs = [fileManager attributesOfItemAtPath:self.tempFilePath error:nil];
self.currentLength = [attrs fileSize];
[request setValue:[NSString stringWithFormat:@"bytes=%lld-", self.currentLength] forHTTPHeaderField:@"Range"];
} else {
self.currentLength = 0;
[fileManager createFileAtPath:self.tempFilePath contents:nil attributes:nil];
}
// 打开文件句柄
self.fileHandle = [NSFileHandle fileHandleForWritingAtPath:self.tempFilePath];
[self.fileHandle seekToEndOfFile];
// 创建任务
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:nil];
self.dataTask = [session dataTaskWithRequest:request];
[self.dataTask resume];
}
// 处理响应
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
if (httpResponse.statusCode == 206) {
// 支持断点续传,继续接收
completionHandler(NSURLSessionResponseAllow);
} else if (httpResponse.statusCode == 200) {
// 不支持,重置文件
self.currentLength = 0;
[self.fileHandle truncateFileAtOffset:0];
completionHandler(NSURLSessionResponseAllow);
} else {
completionHandler(NSURLSessionResponseCancel);
}
}
// 接收数据
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
[self.fileHandle writeData:data];
self.currentLength += data.length;
// 保存进度
[[NSUserDefaults standardUserDefaults] setObject:@(self.currentLength) forKey:@"currentLength"];
}
// 完成或错误处理
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
[self.fileHandle closeFile];
self.fileHandle = nil;
if (!error) {
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSString *destPath = [documentsPath stringByAppendingPathComponent:@"manualDownload.zip"];
[fileManager moveItemAtPath:self.tempFilePath toPath:destPath error:nil];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"currentLength"];
}
}
注意事项
-
服务器兼容性:确保服务器支持
Range请求。 - 进度保存:频繁保存可能导致性能问题,建议适当节流。
- 文件完整性:下载完成后可通过MD5校验确保文件正确。
两种方式对比
| 特性 | NSURLSessionDownloadTask | 手动处理Range头 |
|---|---|---|
| 实现复杂度 | 简单(系统自动管理) | 复杂(需手动处理Range和文件) |
| 灵活性 | 低(依赖系统实现) | 高(完全控制流程) |
| 适用场景 | 快速实现,服务器支持标准断点续传 | 需深度定制或服务器特殊要求 |
| 后台下载支持 | 支持 | 需额外处理 |
常见问题解决
-
resumeData无效:
- 检查URL是否变化。
- 确保服务器资源未修改。
- 重新下载时删除旧的resumeData。
-
文件损坏:
- 下载完成后校验文件哈希值。
- 使用
NSFileManager的原子写入操作。
-
进度不准:
- 在主线程更新UI,避免卡顿。
- 使用
NSProgress类跟踪进度。
通过以上步骤,你可以在Objective-C环境中实现稳定可靠的断点续传功能。根据具体需求选择合适的方法,并注意处理边界条件和错误情况。









网友评论