- 如果不用离线下载,推荐NSURLSessionDownloadTask-delegate实现断点下载。它本身无法实现离线下载功能
- 但一般都是建议用NSURLSessionDataTask-delegate使用断点下载,因为扩展到离线断点下载的扩展性更好。详见“离线断线下载”
使用NSURLSessionDownloadTask-delegate实现大文件下载-监听下载进度
(1)方法缺陷:无法实现离线下载功能
01 如果用户点击暂停之后退出程序,那么需要把恢复下载的数据写一份到沙盒,代码复杂度更高
02 如果用户在下载中途未保存恢复下载数据即退出程序,则不具备可操作性;另外断网等也会使得该功能有问题
(2)实现思路
01 设置url
02 根据url设置请求对象(有需要设置请求体)
03 通过绘画环境设置绘画对象及代理,设置为成员变量
04 创建downLoadTask对象,设置为成员变量
05 开启downLoadTask
06 在获取数据的代理方法中,获取已经下载量/总共要下载量的比例,显示在进度条上
07 提供暂停下载,取消下载,继续下载功能
08 取消下载中,需要通过cancelByProducingResumeData记录resumData数据,是否需要新开任务数据,另外要记得先对resumData做非空判断
09 继续下载功能,要对是否新开任务做非空判断,如果不为空,要根据resumData重新生成新的任务,开启任务;另外要记得清空新开任务数据。
10 下载完成后,将文件移动到合适的地方
(3)实现细节注意
01 通过代理监听下载,不能使用block来发送网络请求
02 取消下载不能直接cancel,不然无法继续下载
03 取消下载要对resumData做非空判断,否则会有bug,因为根据resume创建新的任务,resume为空会崩
04 要添加新开任务成员属性,并在合适的位置赋值,这样才能针对是由于暂停还是取消而执行继续下载任务(前者是从当前进度继续下载,后者是从记录的resume创建新的任务继续下载) 05 需对功能按钮进行设置,不能任意让用户多次点击
@interface ViewController ()<NSURLSessionDownloadDelegate>
@property (weak, nonatomic) IBOutlet UIProgressView *progressView;
/** 下载任务 */
@property (nonatomic ,strong)NSURLSessionDownloadTask *downloadTask;
/** resumData */
@property (nonatomic ,strong)NSData *resumData;
/** session*/
@property (nonatomic ,strong)NSURLSession *session;
/** 是否需要新任务*/
@property (nonatomic ,assign,getter=isNeedNewTask)BOOL needNewTask;
@end
@implementation ViewController
//开始下载
- (IBAction)startBtnClick:(id)sender {
//1.创建session
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
//2.创建task
NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithURL:[NSURL URLWithString:@"http://120.25.226.186:32812/resources/videos/minion_02.mp4"]];
//3.启动task
[downloadTask resume];
self.downloadTask = downloadTask;
self.session = session;
}
//取消
- (IBAction)suspenBtnClcik:(id)sender {
//用这个取消是不可以恢复
// [self.downloadTask cancel];
//如果采取这种方式来取消任务,那么该方法会通过resumeData保存当前文件的下载信息
//只要有了这份信息,以后就可以通过这些信息来恢复下载
//多次点击取消下载,从第二次起resumeData==nil
[self.downloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
//判断resumeData是否为空,若为空则直接返回
if(!resumeData) return;
//通过拿到的resumData可以重新创建一个task下载任务
self.resumData = resumeData;
//设置是否要新开任务
self.needNewTask = YES;
}];
}
//暂停
- (IBAction)suspend {
[self.downloadTask suspend];
}
//继续
- (IBAction)goOnBtnClick:(id)sender {
//判断是否需要新开任务
if (self.isNeedNewTask == YES) {
//根据resumData重新创建一个task下载任务
//self.session可以为空,self.resumData不能为空
self.downloadTask = [self.session downloadTaskWithResumeData:self.resumData];
}
//开启任务
[self.downloadTask resume];
//设置是否要新开任务
self.needNewTask = NO;
}
#pragma mark --------------
#pragma mark NSURLSessionDownloadDelegate
/*当接收到服务器返回的数据的时候调用,该方法用来写数据
bytesWritten:本次写入数据的大小
totalBytesWritten:已经写入到沙盒中的数据的大小
totalBytesExpectedToWrite:文件的总大小 === expectedTotalBytes
*/
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
//获取文件下载进度
self.progressView.progress = 1.0 *totalBytesWritten/totalBytesExpectedToWrite;
}
/*
Resume:恢复
取消之后再次下载就会调用这个方法|恢复下载的时候调用
*/
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes
{
NSLog(@"didResumeAtOffset");
}
/*当下载完成之后会调用这个方法*/
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
{
NSLog(@"didFinishDownloadingToURL");
NSLog(@"%@",location);
//caches
NSString *fullPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:downloadTask.response.suggestedFilename];
//剪切文件
[[NSFileManager defaultManager] moveItemAtURL:location toURL:[NSURL fileURLWithPath:fullPath] error:nil];
NSLog(@"%@",fullPath);
}
/*当整个请求结束的时候会调用*/
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
NSLog(@"didCompleteWithError");
}
4.NSURLConnection实现大文件断点下载
(1)实现思路
1. 大文件下载内存问题:边接收数据边写文件以解决内存越来越大的问题。注意文件句柄或输出流的关闭与清空。
2. 断点下载实现:文件下载默认是从头下载。可通过设置可变request的资源下载范围即可实现断点下载。注意进度计算与显示的细节处理。
(2)实现步骤:
- 设置请求体,设置资源下载范围,向服务器发送请求;每次取消,都要重新发送网络请求。
-
服务器响应时:
- 判断是否已经存在文件,如果是,直接return;否则执行2-4步
- 通过响应头获取总文件大小,服务器建议的文件名
- 获取caches文件夹路径,根据建议的文件名拼接全路径
- 获取文件管理者,创建文件用于保存数据(可以通过输出流实现)
-
服务器返回数据时(多次调用):
- 计算进度并显示
- 根据data.length计算出已下载数据量
- 设置进度条的进度self.progressView.progress = 1.0* self.currentLength/self.totalLength
- 写入文件:
- 根据文件全路径创建文件句柄,让文件句柄写入数据。(每次写入数据,文件句柄会自动接着上次的地方继续写入)(边接受边写入的核心。
- 或者通过输出流实现
- 不能简单的通过data writeToFile,这样每次都会覆盖之前的数据)
- 计算进度并显示
-
请求结束后:
- 文件句柄关闭文件
- 清空文件句柄
提供停止下载功能。NSURLConnection的cancel方法。一旦取消,无法恢复,只能重新发送请求,继续下载。没有暂停功能。
输出流使用介绍。(不是断点下载必要功能)
##设置请求体,设置资源下载范围,向服务器发送请求。
//创建请求对象
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
// 设置下载文件的某一部分
// 只要设置HTTP请求头的Range属性, 就可以实现从指定位置开始下载
/*
表示头500个字节:Range: bytes=0-499
表示第二个500字节:Range: bytes=500-999
表示最后500个字节:Range: bytes=-500
表示500字节以后的范围:Range: bytes=500-
*/
NSString *range = [NSString stringWithFormat:@"bytes=%zd-",self.currentLength];
[request setValue:range forHTTPHeaderField:@"Range"];
##服务器响应时:
//当接收到服务器响应的时候调用,该方法只会调用一次
-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
// 判断当前是否已经下载过,如果当前文件已经存在,那么直接返回
if (self.currentLength >0) return;
//0.获得当前要下载文件的总大小(通过响应头得到)
NSHTTPURLResponse *res = (NSHTTPURLResponse *)response;
self.totalLength = res.expectedContentLength;
//创建一个新的文件,用来当接收到服务器返回数据的时候往该文件中写入数据
//1.获取文件管理者
NSFileManager *manager = [NSFileManager defaultManager];
//2.拼接文件的全路径
//caches文件夹路径
NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
NSString *fullPath = [caches stringByAppendingPathComponent:res.suggestedFilename];
self.fullPath = fullPath;
//3.创建一个空的文件
[manager createFileAtPath:fullPath contents:nil attributes:nil];
//1.创建一个用来向文件中写数据的文件句柄
//注意当下载完成之后,该文件句柄需要关闭,调用closeFile方法
NSFileHandle *handle = [NSFileHandle fileHandleForWritingAtPath:self.fullPath];
self.hadle = handle;
}
##服务器返回数据时(多次调用):
//该方法可能会被调用多次
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
//2.设置写数据的位置(追加)
//[self.handle seekToEndOfFile];自动实现
//3.写数据
[self.handle writeData:data];
//4.计算当前文件的下载进度
self.currentLength += data.length;
NSLog(@"%f",1.0* self.currentLength/self.totalLength);
self.progressView.progress = 1.0* self.currentLength/self.totalLength;
}
##请求结束后:
//当请求结束之后调用
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
NSLog(@"下载完毕---%@",self.fullPath);
[self.handle closeFile];
self.handle = nil;
}
##提供停止下载功能。
- (IBAction)suspendBtnClick:(id)sender {
[self.connect cancel];
}
## 输出流使用介绍
(1)使用输出流也可以实现和NSFileHandle相同的功能
(2)如何使用
//1.创建一个数据输出流(服务器响应时创建并启动)
/*
第一个参数:二进制的流数据要写入到哪里
第二个参数:采用什么样的方式写入流数据,如果YES则表示追加,如果是NO则表示覆盖
*/
NSOutputStream *stream = [NSOutputStream outputStreamToFileAtPath:fullPath append:YES];
self.stream = stream;
//只要调用了该方法就会往文件中写数据
//如果文件不存在,那么会自动的创建一个
[stream open];
//2.当接收到数据的时候写数据(接收服务器数据时)
//使用输出流写数据
/*
第一个参数:要写入的二进制数据类型
第二个参数:要写入的数据的大小
*/
[self.stream write:data.bytes maxLength:data.length];
//3.当文件下载完毕的时候关闭输出流(接收完数据时)
//关闭输出流
[self.stream close];
self.stream = nil;