二、NSURLSession 下载大文件,以下测试我们使用Apache 服务器里面的数据
- 2.1、NSURLSession 简介 以及 简单使用,完整代码在JKNSURLSession中的
Test1ViewController
NSURLSession是在iOS 7.0(15年)的时候推出的,在最开始的时候也会出现峰值,后来解决后大家才重新使用NSURLSession,NSURLSession所有的任务都是session发起的,默认所有任务都是“挂起”的,需要resume执行。
- 简单的使用:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ // 1.创建url: http://localhost/test.json NSURL *url = [NSURL URLWithString:@"http://localhost/test.json"]; [self taskWithUrl:url]; } -(void)taskWithUrl:(NSURL *)url{ [[[NSURLSession sharedSession]dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { // 反序列化 id result = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; NSLog(@"%@",result); }] resume]; }
提示:
NSURLSession
是一个单利,目的是使开发更容易,默认是不启动的,需要开发者调用resume
启动NSURLSession
,如上面
- 2.2、NSURLSession 简单的下载,完整代码在JKNSURLSession中的
Test2ViewController
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ // 1.创建url:http://localhost NSURL *url = [NSURL URLWithString:@"http://localhost/2ittt-zm.mp4"]; [self taskWithUrl:url]; } -(void)taskWithUrl:(NSURL *)url{ /* 如果在回调方法中,不做任何处理,下载的文件会被删除 下载是默认下载到tmp文件夹,系统会自动回收这个区域 设计目的: 1.通常从网络下载文件,什么样的格式文件最多?zip 2.如果是zip包,下载之后要解压 3.解压之后,原始的zip就不需要了。系统会自动帮你删除 */ [[[NSURLSession sharedSession]downloadTaskWithURL:url completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) { NSLog(@"%@",location); }]resume]; }
提示:location打印是:
file:///Users/wangchong/Library/Developer/CoreSimulator/Devices/643379A0-0449-4FE2-AD19-71258BDDBAE6/data/Containers/Data/Application/E6F1AABA-BDBE-4191-A167-02D5DCD19D41/tmp/CFNetworkDownload_OaisFm.tmp
,我们可以看到 tmp,临时存放下载文件的地方
- 2.3、文件解压缩,完整代码在JKNSURLSession中的
Test3ViewController
- (1)、这里我们需要使用一个工具
SSZipArchive
,在demo里面有
- SSZipArchive的功能:
压缩文件
和解压文件
- (2)、解压我们服务器的一个文件到
Library/Caches
里面
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ // 1.创建url:http://localhost NSURL *url = [NSURL URLWithString:@"http://localhost/ftp.docx.zip"]; [self taskWithUrl:url]; } -(void)taskWithUrl:(NSURL *)url{ [[[NSURLSession sharedSession]downloadTaskWithURL:url completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) { NSLog(@"%@",location); // 文件解压目标路径,不能指定目标文件。因为我们解压文件会产生多个文件 NSString *cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)lastObject]; [SSZipArchive unzipFileAtPath:location.path toDestination:cachePath]; }]resume]; }
- 2.4、NSURLSession下载进度监听,完整代码在JKNSURLSession中的
Test4ViewController
- (1)、创建一个 NSURLSession对象
// 全局的网络会话,管理所有的网络任务 @property(nonatomic,strong) NSURLSession *session; -(NSURLSession *)session{ if (!_session) { /** 全局网络环境的一个配置 比如:身份验证,浏览器类型以及缓存,超时,这些都会被记录在 */ NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration]; _session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil]; } return _session; } -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ // 1.创建url:http://localhost NSURL *url = [NSURL URLWithString:@"http://localhost/2ittt-zm.mp4"]; //如果你要监听下载进度,必须使用代理。 //如果你要更进下载进度,就不能block 。 [[self.session downloadTaskWithURL:url]resume]; }
提示:
- 如果你要监听下载进度,必须使用代理。
[NSURLSession sharedSession]
是全局的单例。整个系统都会用,也就是其他的应用程序也会用- 如果你要更进下载进度,就不能block 。
- (2)、常用的代理方法(其中下载完成的方法是在iOS7.0之后必须要写的,在iOS7之前,下面的三个方法都必须写)
/** 1、下载完成的方法 */ -(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location{ NSLog(@"下载完成"); } /** 2、下载续传数据的方法 */ -(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes{ } /** 3、下载进度的方法 @param session 网络会话 @param downloadTask 调用代理方式的下载任务 @param bytesWritten 本次下载的字节数 @param totalBytesWritten 已经下载的字节数 @param totalBytesExpectedToWrite 期望下载的字节数 -- 文件的总大小 */ -(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite{ float progress = (float)totalBytesWritten/totalBytesExpectedToWrite; NSLog(@"进度=%f",progress); }
- 2.5、自定义progressview,完整代码在JKNSURLSession中的
Test5ViewController
- (1)、自定义progressview我们选择继承于 UIButton,原因是:button可以设置文字,展示的时候比较方便,当然也可以使用其他的控件,比如lable,那么我们自定义一个类 JKProgressBtn 继承于UIButton,代码如下
- JKProgressBtn.h里面的代码
#import <UIKit/UIKit.h> NS_ASSUME_NONNULL_BEGIN @interface JKProgressBtn : UIButton /** 表示进度的值 */ @property(nonatomic,assign) float progress; @end NS_ASSUME_NONNULL_END
- JKProgressBtn.m里面的代码
#import "JKProgressBtn.h" @implementation JKProgressBtn -(instancetype)initWithFrame:(CGRect)frame{ self = [super initWithFrame:frame]; if (self) { [self setTitleColor:[UIColor brownColor] forState:UIControlStateNormal]; } return self; } -(void)setProgress:(float)progress{ _progress = progress; // 进度的Label [self setTitle:[NSString stringWithFormat:@"%0.2f%%",_progress*100] forState:UIControlStateNormal]; // 刷新视图 [self setNeedsDisplay]; } -(void)drawRect:(CGRect)rect{ CGSize s = rect.size; // 圆心 CGPoint center = CGPointMake(s.width*0.5, s.height*0.5); // 半径 CGFloat r = (s.height > s.width) ? s.width*0.5:s.height*0.5; r = r - 5; // 其实角度 CGFloat startAngle = -M_PI_2; // 结束角度 CGFloat endAngle = self.progress*2*M_PI + startAngle; /** 第1个参数:圆心 第2个参数:半径 第3个参数:起始角度 第4个参数:结束角度 第5个参数:YES:顺时针 / NO:逆时针 */ UIBezierPath *bezierPath = [UIBezierPath bezierPathWithArcCenter:center radius:r startAngle:startAngle endAngle:endAngle clockwise:YES]; // 圆环的宽度 bezierPath.lineWidth = 10.0; // 设置圆环的样式(圆形) bezierPath.lineCapStyle = kCGLineCapRound; // 给圆环添加颜色 [[UIColor purpleColor]setStroke]; // 绘制路径 [bezierPath stroke]; } @end
提示:
UIBezierPath
:贝塞尔曲线的起始角度是时钟的3点,也就是数学上x的正轴方向,故上面我们把起始角度设置为-M_PI_2
,也就是 时钟的12点,同理数学上y的正轴方向,其他的参数在上面描述的很清楚
- 解释一下:
bezierPath.lineWidth = 10.0;
,在贝塞尔曲线里面,半径决定后,圆环的宽度是以半径向外扩展的,所以才有上面的:r = r - 5;
- (2)、JKProgressBtn 的使用,在NSURLSession下载进度的方法里面刷新JKProgressBtn的进度,如下:
- 先在控制器里面定义一个 JKProgressBtn属性并初始化添加到控制器
// 进度的View @property(nonatomic,strong) JKProgressBtn *progressView; -(JKProgressBtn *)progressView{ if (!_progressView) { _progressView = [[JKProgressBtn alloc]initWithFrame:CGRectMake([UIScreen mainScreen].bounds.size.width/2-50, [UIScreen mainScreen].bounds.size.height/2-50, 100, 100)]; _progressView.backgroundColor = [UIColor yellowColor]; } return _progressView; } // 添加进度View [self.view addSubview:self.progressView];
- 在下载进度的方法里面设置主线程刷新
progressView
的值
/** 3、下载进度的方法 @param session 网络会话 @param downloadTask 调用代理方式的下载任务 @param bytesWritten 本次下载的字节数 @param totalBytesWritten 已经下载的字节数 @param totalBytesExpectedToWrite 期望下载的字节数 -- 文件的总大小 */ -(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite{ float progress = (float)totalBytesWritten/totalBytesExpectedToWrite; NSLog(@"进度=%f",progress); dispatch_async(dispatch_get_main_queue(), ^{ self.progressView.progress = progress; }); }
提示:
NSURLSession
创建的下载是在子线程执行的,所以上面才在主线程刷新UI
- 2.6、断点续传,完整代码在JKNSURLSession中的
Test6ViewController
- (1)、创建三个按钮,开始下载、暂停下载,继续下载
- (2)、创建一个全局的下载任务
/** 设置一个全局的下载任务 */ @property(nonatomic,strong) NSURLSessionDownloadTask *downloadTask;
- (3)、开始下载、暂停下载,继续下载 三个方法对应的代码如下
#pragma mark 开始下载 -(void)startLoadTask{ NSLog(@"开始下载"); // 1.创建url:http://localhost NSURL *url = [NSURL URLWithString:@"http://localhost/2ittt-zm.mp4"]; self.downloadTask = [self.session downloadTaskWithURL:url]; // 2、执行下载 [self.downloadTask resume]; } #pragma mark 暂停下载 -(void)pauseLoadTask{ NSLog(@"暂停下载"); [self.downloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) { self.resumeData = resumeData; //释放任务 self.downloadTask = nil; }]; } #pragma mark 继续下载 -(void)resumeLoadTask{ NSLog(@"继续下载"); // 防止继续下载被执行两次,故下面把self.resumeData赋为nil if (self.resumeData == nil) return; self.downloadTask = [self.session downloadTaskWithResumeData:self.resumeData]; self.resumeData = nil; [self.downloadTask resume]; }
提示:断点续传其实也就是在暂停下载的时候获取下载的
resumeData
,再次接着下载的时候,用resumeData
再获取一个NSURLSessionDownloadTask
,从而接着下载
- 2.7、NSURLSession 强引用 问题
- (1)、
NSURLSession
是一个强引用,在下载完成的时候要进行释放,不管是是否支持不在当前界面下载,当所有的下载任务都完成后,需要进行释放 session,并赋nil,否则会造成内存泄漏
/** 1、下载完成的方法 */ -(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location{ NSLog(@"下载完成"); [self.session finishTasksAndInvalidate]; self.session = nil; }
- (2)、如果只支持当前界面下载的情况,退出当前界面,取消下载,session 赋nil
[self.session invalidateAndCancel]; self.session = nil;
- 2.8、完整代码请看 JKNSURLSession 中的
Test6ViewController
三、下载管理器 demo地址:JKDownloaderKit
- 3.1、下载思路
- (1)、创建另两个文件夹:
JKDownloadCompleted
(下载完成的文件夹)和JKDownLoading
(下载中的文件夹),在下载中的资源会存在JKDownLoading
中,下载完成后会移动到JKDownloadCompleted
里面
- (2)、先查看服务器上的文件大小
- (3)、查看本地是否存在文件,如果存在如下
- 如果文件小于服务器文件的大小,从本地文件长度开始下载(断点续传)
- 如果文件等于服务器文件的大小,再把文件生成一个MD5与服务器对文件返回的MD5做对比,如果一样,代表下载完成
- 如果文件大于服务器文件的大小,发生错误,直接删除文件,重新下载
- (4)、如果本地不存在该文件,直接下载
- 上传视频的思路:
在上传视频的时候,如果视频断开了(程序退出了),那么就要去服务器请求看看自己之前上传了多少,接着上传就好,和视频的下载原理是一样的,对比- 总体思路图
- (5)、在监听下载的方法中,当下载完成后做如下的操作
- 在没有
error
的情况下,文件下载是完毕了,但是不一定成功,分析如下
- 判断, 本地缓存
==
文件总大小 (如果不相等,说明下载有问题,删除下载路径下的文件,重新下载;如果相等在验证文件内容的MD5值是否一样,一样的话才是真正的下载完成,否则下载是有问题的,删除下载路径下的文件,重新下载)
- 下载-文件完整性验证机制:验证文件的合法性, 下载数据是否完整可用
- 服务器返回文件下载信息的同时, 会返回该文件内容的md5值
- 本地下载完成后, 可以, 在本地已下载的文件的MD5值和服务器返回的进行对比;
- 为了简单, 有的, 服务器返回的下载文件MD5值, 直接就是命名称为对应的文件名称
- 3.2、创建一个管理下载的类
- 命名下载的类为:
JKDownLoader
,继承于NSObject
,定义一个下载的方法
/** 定义一个下载的方法 (1)、先查看服务器上的文件大小 (2)、查看本地是否存在文件,如果存在如下 2.1、如果文件小于服务器文件的大小,从本地文件长度开始下载(断点续传) 2.2、如果文件等于服务器文件的大小,再把文件生成一个MD5与服务器对文件返回的MD5做对比,如果一样,代表下载完成 2.3、如果文件大于服务器文件的大小,发生错误,直接删除文件,重新下载 (3)、如果本地不存在该文件,直接下载 @param url 下载的url */ -(void)downloadWithUrl:(NSURL *)url{ }
- 3.3、从 服务器 获取下载文件的信息
- 我们需要设置下载的总大小以及下载后存放的位置
/** 文件的总大小 */ @property(nonatomic,assign) long long expectdContentLength; /** 文件的下载路径 */ @property(nonatomic,strong) NSString *downloadPath;
- 获取文件信息的私有ipa
#pragma mark 私有方法 -(void)selectServerFileInfoWithUrl:(NSURL *)url{ // 1.请求信息:我们只需要获取头部信息就好 NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:1 timeoutInterval:kTimeOut]; request.HTTPMethod = @"HEAD"; // 2.建立网络连接 NSURLResponse *response = nil; [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:NULL]; NSLog(@"%@ %@ %lld",data,response,response.expectedContentLength); // 3.记录服务器的文件信息 // 3.1、文件长度 self.expectdContentLength = response.expectedContentLength; // 3.2、建议保存的文件名字,将在文件保存在tmp,系统会自动回收 self.downloadPath = [NSTemporaryDirectory() stringByAppendingPathComponent:response.suggestedFilename]; }
- 提示:这里采用的是同步方法,因为我们需要根据文件的信息去操作下面的下载操作,不能使用异步
NSURLConnection
:默认是在 主线程 进行操作,而NSURLSession
是在 子线程 进行操作
- 3.4、检查 本地 下载文件的信息
/** 2.从本地检查要下载的文件信息(除了文件下载完,其他的情况都需要下载文件) @return YES:需要下载,NO:不需要下载 */ -(BOOL)checkLocalFileInfo{ long long fileSize = 0; // 1.判断文件是否存在 if ([[NSFileManager defaultManager]fileExistsAtPath:self.downloadPath]) { // 获取本地存在文件大小 NSDictionary *attributes = [[NSFileManager defaultManager]attributesOfItemAtPath:self.downloadPath error:NULL]; NSLog(@"%@",attributes); fileSize = [attributes[NSFileSize] longLongValue]; } // 2.根据文件大小来判断文件是否存在 if(fileSize > self.expectdContentLength){ // 文件异常,删除该文件 [[NSFileManager defaultManager]removeItemAtPath:self.downloadPath error:NULL]; fileSize = 0; }else if (fileSize == self.expectdContentLength) { // 文件已经下载完 return NO; } return YES; }
- 3.5、文件下载实现
- (1)、定义一个属性保存下载的地址 URL
/** 视频的下载地址 URL */ @property(nonatomic,strong) NSURL *downloadUrl;
- (2)、视频下载从当前的字节开始下载,不管字节是不是0,都是检查过本地路径的字节,本地有的话,当前字节就不是0,也就是断点续传;没有的话就是0,也就是从头开始下载
- 拓展一个 HTTP 属性 Range,下载会用到
Bytes = 0-499 : 从 0 到 499 的 500 个字节 Bytes = 500-999 : 从500-999的第二个500字节 Bytes = 500- : 从500开始到以后所有的字节 Bytes = -500 最后500个字节 Bytes = 500-699,800-1299,1600-2000 同时指定多个范围
- (3)、开始下载,这里先使用上面
一
中的NSURLConncetion
放在子线程,开启Runloop的代理方法来下载,把 NSURLConncetion 放在异步并发的队列,用文件流拼接写入路径,下面只展示部分代码,完整代码看 demo
/* 保存文件的输出流 - (void)open; 写入之前,打开流 - (void)close; 写入完毕之后,关闭流 */ @property(nonatomic,strong)NSOutputStream *fileStream; /* 保存下载线程的运行循环,也就是下载任务的 runloop */ @property(nonatomic,assign)CFRunLoopRef downloadRunloop; #pragma mark 下载文件 -(void)downloadFile{ dispatch_async(dispatch_get_global_queue(0, 0), ^{ // 1.建立请求 NSMutableURLRequest *request = [[NSMutableURLRequest alloc]initWithURL:self.downloadUrl cachePolicy:1 timeoutInterval:MAXFLOAT]; // 2.设置下载的字节范围,self.currentLength到之后所有的字节 NSString *downloadRangeStr = [NSString stringWithFormat:@"bytes=%lld-",self.currentContentLength]; // 3.设置请求头字段 [request setValue:downloadRangeStr forHTTPHeaderField:@"Range"]; // 4.开始网络连接 NSURLConnection *connection = [NSURLConnection connectionWithRequest:request delegate:self]; // 5.设置代理工作的操作 [[NSOperationQueue alloc]init] 默认创建一个异步并发队列 [connection setDelegateQueue:[[NSOperationQueue alloc]init]]; // 启动连接 [connection start]; //5.启动运行循环 /* CoreFoundation 框架 CFRunLoop CFRunloopStop() 停止指定的runloop CFRunloopGetCurrent() 获取当前的Runloop CFRunloopRun() 直接启动当前的运行循环 */ // (1)、拿到当前的运行循环 self.downloadRunloop = CFRunLoopGetCurrent(); // (2)、启动当前的运行循环 CFRunLoopRun(); }); } #pragma mark NSURLConnection的代理NSURLConnectionDataDelegate的方法 // 1、接收服务器的响应 --- 状态和响应头做一些准备工作 - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{ self.fileStream = [[NSOutputStream alloc]initToFileAtPath:self.downloadPath append:YES]; [self.fileStream open]; } // 2、接收服务器的数据,由于数据是分块发送的,这个代理方法会被执行多次,因此我们也会拿到多个data - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{ // 接收数据,用输出流拼接,计算下载进度 // 将数据拼接起来,并判断是否可写如,一般情况下可写入,除非磁盘空间不足 if ([self.fileStream hasSpaceAvailable]) { [self.fileStream write:data.bytes maxLength:data.length]; } // 当前长度拼接 self.currentContentLength += data.length; // 计算百分比 // progress = (float)long long / long long float progress = (float)self.currentContentLength/self.expectdContentLength; // 传送百分比 if (self.progress) { self.progress(progress); } NSLog(@"%f %@",progress,[NSThread currentThread]); } // 3、接收到所有的数据下载完毕后会有一个通知 - (void)connectionDidFinishLoading:(NSURLConnection *)connection; { NSLog(@"下载完毕"); [self.fileStream close]; // 下载完成的回调 if (self.completion) { self.completion(self.downloadPath); } // 关闭当前下载完的 RunLoop CFRunLoopStop(self.downloadRunloop); } // 4、下载错误的处理 -(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{ NSLog(@"连接失败:%@",error.localizedDescription); // 关闭流 [self.fileStream close]; // 关闭当前的 RunLoop CFRunLoopStop(self.downloadRunloop); }
- 3.6、暂停下载操作
暂停下载操作直接调用:NSURLConnection
的cancel
就好 - 3.7、多文件下载管理我们创建一个下载管理器
JKDownloaderManger
,设置成单利,用来下载多个文件,同时创建下载缓存池,避免多次下载同一个文件
- (1)、单利的实现(一个静态变量,三个方法,才是完整的单利)
static id instance; +(instancetype)allocWithZone:(struct _NSZone *)zone{ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [super allocWithZone:zone]; }); return instance; } -(id)copy{ return instance; } +(instancetype)shareDownloaderManger{ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [[self alloc]init]; }); return instance; }
- (2)、创建字典缓存池
@property(nonatomic,strong) NSMutableDictionary *downloadCache;
- (3)、下载方法的实现( 以NSURL的url.path 为键 )
-(void)jk_downloadWithUrl:(NSURL *)url withDownProgress:(downProgress)progress completion:(downCompletion)completion fail:(downFailed)failed { // 1.判断缓存池里面是否有同一个下载任务 JKDownLoader *downLoader = self.downloadCache[url.path]; if (downLoader != nil) { NSLog(@"已经在下载列表中,请不要重复下载"); return; } // 2.创建新的下载任务 downLoader = [[JKDownLoader alloc]init]; // 3.将下载任务添加到缓存池 [self.downloadCache setValue:downLoader forKey:url.path]; __weak typeof(self) weakSelf = self; [downLoader jk_downloadWithUrl:url withDownProgress:progress completion:^(NSString * _Nonnull downFilePath) { // 1.将下载从缓存池移除 [weakSelf.downloadCache removeObjectForKey:url.path]; // 2.执行调用方法的回调 if (completion) { completion(downFilePath); } } fail:failed]; }
- (4)、下载暂停:暂停下载,从缓存池移除该url的path
#pragma mark 暂停某个文件下载 -(void)pauseloadWithUrl:(NSURL *)url{ // 1.通过url获取下载任务 JKDownLoader *downLoader = self.downloadCache[url.path]; // 2.暂停下载 if (downLoader == nil){ if (self.failed) { self.failed(@"已经暂停下载,请不要重复点击"); } return; } [downLoader pause]; // 3.从缓存池移除 [self.downloadCache removeObjectForKey:url.path]; }
iOS 下载器完整的代码请查看demo
到此下载完毕,下一篇会阐述 下载中
和 下载完成
文件夹里面文件的读取,敬请关注