断点续传

断点续传的原理

断点续传允许在下载中断后从中断处继续下载,其核心依赖于 HTTP协议的Range请求头

  1. Range头机制:客户端通过Range: bytes=start-头告知服务器需要从哪个字节开始传输数据。
  2. 服务器响应:若服务器支持,返回206 Partial Content状态码及对应数据块;否则返回200 OK及完整数据。
  3. 本地文件管理:客户端需记录已下载大小,将新数据追加到文件末尾,确保中断后能恢复。

实现方式一:使用 NSURLSessionDownloadTask 和 ResumeData

步骤说明

  1. 创建下载任务:初始化NSURLSessionDownloadTask
  2. 暂停并保存ResumeData:调用cancelByProducingResumeData:获取恢复数据。
  3. 持久化ResumeData:将resumeData保存到磁盘或UserDefaults。
  4. 恢复下载:用resumeData重新创建任务继续下载。
  5. 完成处理:移动临时文件到目标路径。

代码示例

// 定义属性
@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)

步骤说明

  1. 检查本地文件:获取已下载大小。
  2. 设置Range头:构造请求时指定bytes=start-
  3. 处理服务器响应
    • 206:追加写入数据。
    • 200:重新下载(服务器不支持Range)。
  4. 实时保存进度:每次写入后更新已下载大小。
  5. 完成下载:移动文件并清理状态。

代码示例

// 定义属性
@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和文件)
灵活性 低(依赖系统实现) 高(完全控制流程)
适用场景 快速实现,服务器支持标准断点续传 需深度定制或服务器特殊要求
后台下载支持 支持 需额外处理

常见问题解决

  1. resumeData无效

    • 检查URL是否变化。
    • 确保服务器资源未修改。
    • 重新下载时删除旧的resumeData。
  2. 文件损坏

    • 下载完成后校验文件哈希值。
    • 使用NSFileManager的原子写入操作。
  3. 进度不准

    • 在主线程更新UI,避免卡顿。
    • 使用NSProgress类跟踪进度。

通过以上步骤,你可以在Objective-C环境中实现稳定可靠的断点续传功能。根据具体需求选择合适的方法,并注意处理边界条件和错误情况。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容