使用NSURLSessionDataTask实现大文件离线断点下载(完整)
(1)实现思路
1. 大文件下载内存问题:边接收数据边写文件以解决内存越来越大的问题。注意文件句柄或输出流的关闭与清空。
2. 断点下载实现:文件下载默认是从头下载。可通过设置可变request的资源下载范围即可实现断点下载。注意进度计算与显示的细节处理。
3. 离线下载实现:每次下载任务时将文件总大小写入到沙盒中,下次从沙盒中读取;通过fileManager获取已下载文件大小继续下载。根据已下载大小和总大小计算初始进度并显示
(2)实现步骤:
1. viewDidLoad中,获取总文件大小,对总文件大小进行非空判断,若空,则不需要设置下载进度;否则计算并显示下载进度
2. 懒加载dataTask,内部设置请求体,设置资源下载范围,向服务器发送请求;每次取消,都要重新生成dataTask,发送网络请求。
3. 懒加载回话对象,内部根据configuration生成回话对象并设置代理
4. 懒加载已下载文件大小,内部通过文件管理者获取沙盒中已下载文件大小
5. 服务器响应时:
1. 判断是否已经存在文件总大小,如果不存在,获取文件总大小,并写入到沙盒中
2. 通过响应头获取服务器建议的文件名并写入到沙盒中(或直接用宏定义记录文件名)
3. 获取caches文件夹路径,根据建议的文件名或宏拼接全路径
4. 创建输出流并开启,记录成员变量数据(也可以通过获取文件管理者,创建文件用于保存数据),注意非空判断,如果已存在输出流,直接跳过;
5. 实现blcok,允许接收数据
6 服务器返回数据时(多次调用):
1. 计算进度并显示
1 根据data.length计算出已下载数据量
2 设置进度条的进度self.progressView.progress = 1.0* self.currentLength/self.totalLength
2. 写入文件:
1 让输出流写入数据。(或者通过创建文件句柄,让文件句柄写入数据,它每次写入数据,都会自动接着上次的地方继续写入)(边接受边写入的核心)。
2 不能简单的通过data writeToFile,这样每次都会覆盖之前的数据)
7 请求结束后:
1. 关闭输出流
2. 清空输出流对象
8 提供暂停下载、取消下载,继续功能。
1. 暂停下载使用系统的暂停功能
2. 取消下载,清空dataTask与currentSize。
3. 继续下载,重新resume。
9 屏幕跳转或屏幕即将消失时,需要使回话对象不可用并清空回话成员属性
(2)实现细节:
1. viewDidLoad中,获取已下载文件大小及总文件大小,显示下载进度;要对总文件大小做非空判断,为空直接返回
2. 懒加载已下载文件大小,内部通过文件管理者获取文件的属性--大小。文件名可以自取,也可以以服务器建议的名字保存,若通过这种方式,在保存总文件大小时也要保存建议的文字(字典要进行非空判断)
3. 总文件大小获取:内部通过读取沙盒中保存的文件大小获取数据,读取字典数据时,要对字典进行非空判断,若为空,则表示文件未创建;
4. 总文件大小保存:在首次发送网络请求并接收数据时写入磁盘,也需要对字典进行非空判断,字典为空,则表示文件未创建,需要创建可变字典再添加数据
5. 懒加载回话对象,这样就不会由于每次要创建任务而不断创建回话对象
6. 懒加载下载任务,内部设置可变请求对象,请求下载范围由已下载文件大小决定;要对进度进行非空判断,如果为1,即下载完成,就没必要再新建下载任务,直接return nil
7. 服务器响应时,需要对总文件大小做非空判断,如果不为空,不用再次记录文件总大小并保存;对输出流进行非空判断,为空才创建
8. 接收数据时,已下载文件大小需要不断与data.length求和计算;输出流要写入数据
9. 请求结束时,需要关闭输出流,清空输出流;
10. 需在屏幕跳转或者屏幕消失时关闭回话对象并清空回话成员变量,否则会与控制器循环引用,造成内存泄露。
11. 取消任务:需清空dataTask与currentSize,重新根据沙盒中已下载文件大小发送网络请求。否则取消后再继续下载的视频质量非常差
#import "ViewController.h"
#define KfileName @"minion_02.mp4"
#define KtotalSizeFileName @"fzq.fzq"
#define BASEURL (@"http://120.25.226.186:32812/resources/videos/minion_02.mp4")
@interface ViewController ()
/** 输出流 */
@property (nonatomic ,strong)NSOutputStream *stream;
/** 文件的总大小*/
@property(nonatomic ,assign)NSInteger totalSize;
/** 已经下载的文件的大小*/
@property(nonatomic ,assign)NSInteger currentSize;
/** session回话对象 */
@property (nonatomic ,strong)NSURLSession *session;
/** dataTask 任务*/
@property (nonatomic ,strong)NSURLSessionDataTask *dataTask;
/** 进度条 */
@property (weak, nonatomic) IBOutlet UIProgressView *progressView;
@end
@implementation ViewController
-(void)viewDidLoad
{
[super viewDidLoad];
self.view.backgroundColor = [UIColor blackColor];
//获取文件的总大小
NSInteger totalLength = [self getTotalSize];
//计算已经下载了多少,也就是文件的下载进度
if (totalLength == 0) {
NSLog(@"说明第一次下载");
}else
{
self.progressView.progress = 1.0 *self.currentSize/totalLength;
}
}
#pragma mark -----------------
#pragma mark lazy loading
-(NSInteger)currentSize
{
if (!_currentSize) {
// 1.拼接文件的全路径
NSString *fullPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:KfileName];
//可以拿到文件
NSFileManager *manager = [NSFileManager defaultManager];
NSDictionary *fileDict =[manager attributesOfItemAtPath:fullPath error:nil];
_currentSize = [fileDict[@"NSFileSize"] integerValue];
}
return _currentSize;
}
-(NSURLSession *)session
{
if (_session == nil) {
//1.创建session
/*
第一个参数:配置信息
第二个参数:代理 谁成为代理
第三个参数:队列 决定代理方法在哪个线程
*/
_session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
}
return _session;
}
-(NSURLSessionDataTask *)dataTask
{
if (_dataTask == nil) {
//判断当前文件是否已经下载完成了
if (self.progressView.progress == 1.0) {
NSLog(@"文件已经下载完成了");
return nil;
}
//2.创建task
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:BASEURL]];
NSString *range = [NSString stringWithFormat:@"bytes=%zd-",self.currentSize];
[request setValue:range forHTTPHeaderField:@"Range"];
_dataTask = [self.session dataTaskWithRequest:request];
}
return _dataTask;
}
#pragma mark -----------------
#pragma mark Metheds
//保存文件的总大小到沙盒中
-(void)saveTotalSize
{
//1.拼接文件的全路径(xmg.xmg文件存放在什么地方)
NSString *totalSizefullPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:KtotalSizeFileName];
//2.把字典写入到沙盒中
/*这里有两种情况
可能字典已经存在:那么取出的字典有值,可以直接使用
可能字典不存在:那么取出的字典为空,需要重新创建
*/
//根据路径把字典取出来
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithContentsOfFile:totalSizefullPath];
//对字典做非空判断,为空则创建可变字典
if (dict == nil) dict = [NSMutableDictionary dictionary];
//3.修改字典(添加键值对)
[dict setObject:@(self.totalSize) forKey:KfileName];
//4.需要把字典回写
[dict writeToFile:totalSizefullPath atomically:YES];
}
//获得文件的总大小
-(NSInteger )getTotalSize
{
//1.获取文件的全路径(xmg.xmg)
NSString *totalSizefullPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:KtotalSizeFileName];
//2.根据全路径获得沙盒中的字典
/*
字典可能不存在(没有下载过):那么说明之前该文件没有被下载过,直接返回0
字典存在:返回对应文件的文件总大小
*/
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithContentsOfFile:totalSizefullPath];
//对字典进行判断,输出对应结果
if (dict == nil) {
return 0 ;
}else
{
//输出文件记录的下载文件总大小
return [dict[KfileName]integerValue];
}
}
#pragma mark -----------------
#pragma mark Events
//开始下载
- (IBAction)startBtnClcik:(id)sender
{
[self.dataTask resume];
}
//暂停下载
- (IBAction)suspendBtnClick:(id)sender
{
[self.dataTask suspend];
}
//继续下载
- (IBAction)goOnBtnClick:(id)sender
{
NSLog(@"%@",self.dataTask);
[self.dataTask resume];
}
//取消下载
- (IBAction)cancel {
[self.dataTask cancel];
//需清空下列数据,重新根据沙盒中已下载文件大小发送网络请求。否则取消后再继续下载,能够下载数据,但下载完的视频质量非常差
self.dataTask = nil;
self.currentSize = (NSInteger )nil;
}
//跳转
- (IBAction)push {
//释放回话对象,否则会出现循环引用;或者在viewWillDisappear中释放
[self.session invalidateAndCancel];
self.session = nil;
[UIApplication sharedApplication].keyWindow.rootViewController = nil;
}
#pragma mark -----------------
#pragma mark NSURLSessionDataDelegate
/*1.当接收到服务器响应的时候调用*/
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
if (!_totalSize) {
//1.拿到文件的总大小
self.totalSize = response.expectedContentLength;
//2.把文件的总大小以写文件的方式保存到沙盒中
[self saveTotalSize];
}
//判断输出流是否存在,不存在才创建
if (!_stream) {
//3.拼接文件的全路径
NSString *fullPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:KfileName];
//4.创建输出流
NSOutputStream *stream = [[NSOutputStream alloc]initToFileAtPath:fullPath append:YES];
self.stream = stream;
//5.打开输出流
[self.stream open];
}
//6.通过该block告诉系统要如何处理服务器返回给我们的数据
/*
NSURLSessionResponseCancel = 0, //取消,不接受数据
NSURLSessionResponseAllow = 1, //接收
NSURLSessionResponseBecomeDownload = 2, //变成下载请求
NSURLSessionResponseBecomeStream //变成stream
*/
completionHandler(NSURLSessionResponseAllow);
}
/*2.当接收到服务器返回的数据的时候调用,可能被调用多次*/
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
//1.使用输出流往文件中写数据
[self.stream write:data.bytes maxLength:data.length];
//2.计算文件下载进度
self.currentSize +=data.length;
//3.显示进度
self.progressView.progress = 1.0 * self.currentSize/self.totalSize;
}
/*3.当请求结束的时候调用,如果请求失败,那么error有值*/
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
NSLog(@"didCompleteWithError");
//关闭输出流
[self.stream close];
self.stream = nil;
}