利用NSURLSessionDownloadTask实现下载的优缺点
对于iOS中的文件下载功能,苹果为我们提供了NSURLSessionDownloadTask这个类来完成。NSURLSessionDownloadTask支持block下载和代理下载两种方式。Block下载方式不适合大文件下载,因为该方法需要等到文件下载完毕才会回调completionHandler后面的block参数,然后才能获取到location、response、error等信息。这样的话,对于大文件,我们就无法实时的在下载过程中获取文件的下载进度了。而对于代理下载方式来说,则完美解决了这个问题,我们可以在代理方法中实时获取下载进度。
对于断点续传,NSURLSessionDownloadTask也提供了cancelByProducingResumeData:^(NSData * _Nullable resumeData)
以便在取消下载时保存数据。并提供了downloadTaskWithResumeData:(nonnull NSData *)
来根据resumeData进行断点续传。然而,这两个方法只能处理用户手动点击了"取消下载"按钮的情况。实际情况中往往是用户直接退出应用程序而一般不会事先去点击一下"取消按钮",如果是这样的话,resumeData并没有存储,就无法实现重启应用程序之后再继续下载。
利用NSURLSessionDataTask实现大文件下载
鉴于以上情况,实际应用中,我们大多采用NSURLSessionDataTask的代理方式来完成大文件下载功能。代码如下:
#pragma mark - getter
- (NSURLSessionDataTask *)dataTask
{
if (!_dataTask) {
//创建文件夹
_directoryPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:@"YDDownloads"];
if (![[NSFileManager defaultManager] fileExistsAtPath:_directoryPath]) {
[[NSFileManager defaultManager] createDirectoryAtPath:_directoryPath withIntermediateDirectories:YES attributes:nil error:nil];
}
//获取指定路径文件的大小
NSString *fileName = [[NSUserDefaults standardUserDefaults] objectForKey:@"fileName"];
_filePath = [self.directoryPath stringByAppendingPathComponent:fileName];
NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:_filePath error:nil];
_receivedLength = [attributes[@"NSFileSize"] longLongValue];
//创建NSURLSession
_session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
NSURL *url = [NSURL URLWithString:@"http://dldir1.qq.com/qqfile/QQforMac/QQ_V6.3.0.dmg"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
NSString *range = [NSString stringWithFormat:@"bytes=%zd-", _receivedLength];
[request setValue:range forHTTPHeaderField:@"Range"];
_dataTask = [_session dataTaskWithRequest:request];
}
return _dataTask;
}
#pragma mark - 事件响应
//开始/恢复下载
- (IBAction)startTask:(id)sendder
{
[self.dataTask resume];
}
//暂停任务
- (IBAction)suspendTask:(id)sender
{
[self.dataTask suspend];
}
//取消任务
- (IBAction)cancelTask:(id)sender
{
[self.dataTask cancel];
self.dataTask = nil;
}
#pragma mark - NSURLSessionDataDelegate
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler
{
//创建文件
_filePath = [self.directoryPath stringByAppendingPathComponent:response.suggestedFilename];
if (![[NSFileManager defaultManager] fileExistsAtPath:_filePath]) {
[[NSFileManager defaultManager] createFileAtPath:_filePath contents:nil attributes:nil];
[[NSUserDefaults standardUserDefaults] setObject:response.suggestedFilename forKey:@"fileName"];
}
//参数赋初值
_lastDate = [NSDate date];
_expectedLength = response.expectedContentLength + self.receivedLength;
//文件句柄指向指定路径
_fileHandle = [NSFileHandle fileHandleForWritingAtPath:_filePath];
[_fileHandle seekToEndOfFile];
completionHandler(NSURLSessionResponseAllow);
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data
{
//写入数据
[_fileHandle writeData:data];
//计算下载进度
_receivedLength += data.length;
CGFloat taskProgress = _receivedLength * 1.0 / _expectedLength;
NSLog(@"下载进度:%.2f", taskProgress);
//计算下载速度
_accumulateLength += data.length;
if ([[NSDate date] timeIntervalSinceDate:_lastDate] >= 1) {
CGFloat taskSpeed = _accumulateLength * 1.0;
NSLog(@"下载速度:%.2fM/s", taskSpeed / 1024 / 1024);
_lastDate = [NSDate date];
_accumulateLength = 0;
}
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(nullable NSError *)error
{
//关闭文件句柄
[_fileHandle closeFile];
_fileHandle = nil;
//清空参数
self.dataTask = nil;
_accumulateLength = 0;
}
这样一方面我们可以在代理方法中实时获取文件下载进度及速度等信息,另一方面对于断点续传,我们可以在请求头中进行处理,即上述代码中的
NSString *range = [NSString stringWithFormat:@"bytes=%zd-", _receivedLength];
[request setValue:range forHTTPHeaderField:@"Range"];
只要先获取到本地文件的大小,即可根据当前文件大小来请求剩余的数据。
不同于NSURLSessionDownloadTask,利用NSURLSessionDataTask方式实现的下载需要我们手动写入数据,这里采用文件句柄类NSFileHandle 来实现。不过要注意的是,在写入前一定要调用seekToEndOfFile
将文件句柄指向文件的末尾,否则可能造成文件写入位置错误。同时,在下载完成后,还要记得调用closeFile
关闭文件句柄。
结语
通常,我们在工作中还会遇到队列下载的情况。对于队列下载,通常的做法是采用数组来实现。关于具体实现细节,我写了一个demo,有兴趣的朋友可以前往我的github查看:Github地址
网友评论