NSURLConnection简介
- NSURLConnection是2003年随着第一版Safari的发布而发布的,它不单单是一个网络请求类,而是指代Foundation框架的URL系统中的一系列关联的组件:NSURLRequest、NSURLResponse、NSURLProtocol、NSHTTPCookieStorage、NSURLCredentialStorage以及同名类NSURLConnection。
- 从iOS9开始,NSURLConnection中发送请求的两个方法已经过期(同步请求,异步请求),初始化网络连接的方法也被设置为过期,系统不再推荐使用,苹果建议使用NSURLSession发送网络请求。
简单下载
使用NSURLConnection实现简单下载只需三步
NSURL *url = [NSURL URLWithString:@"http://127.0.0.1/videos/IMG_3928.MOV"];
//创建请求对象request
/*
1. cachePolicy - 缓存策略
- NSURLRequestUseProtocolCachePolicy = 0,
-(常用)默认缓存策略,若使用requestWithURL方法,默认使用该缓存策略;它会根据HTTP头中的信息进行缓存处理,服务器可以在HTTP头中加入Expires和Cache-Control等来告诉客户端应该施行的缓存策略。
- NSURLRequestReloadIgnoringLocalCacheData = 1,
-(偶尔使用)顾名思义,忽略本地缓存,直接加载服务器数据
- NSURLRequestReturnCacheDataElseLoad = 2,
-(不用)一直尝试读取缓存数据,若没有缓存,才会去请求网络,该策略的重大缺陷是无法直到缓存的刷新时机。
- NSURLRequestReturnCacheDataDontLoad = 3,
- (不用)该策略之读取缓存数据,无论何时都不会进行网络请求。
2. timeoutInterval - 超时时间 一般设置在15-30秒 AFNetworking中超时时间默认60s
*/
NSURLRequest *req = [NSURLRequest requestWithURL:url cachePolicy:(NSURLRequestUseProtocolCachePolicy) timeoutInterval:15];
//连接服务器,发送网络请求
/*
queue - 这里使用主线程还是子线程由执行的代码块决定
该参数决定block代码块在哪个线程上执行,若block中有刷新UI的操作,则必须放在主线程上执行;若有一些耗时操作,则放在子线程上执行
*/
//开始下载
[NSURLConnection sendAsynchronousRequest:req queue:[[NSOperationQueue alloc] init] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
//下载完成,将数据写入磁盘
/*
atomically 原子属性,保证线程安全
*/
[data writeToFile:@"/Users/jsby-yf007/Desktop/test.MOV" atomically:YES];
NSLog(@"下载完成");
}];
下载完成后,可以在命令行通过获取文件的MD5来验证文件是否下载完整上面代码在实际开发中所带来的问题
1.内存会暴涨,出现一个峰值
出现图2的情况是因为NSURLConnection下载文件时,先是将整个文件下载到内存,然后再写入到沙盒,如果文件比较大,就会出现内存暴涨的情况。在执行
[data writeToFile:@"/Users/jsby-yf007/Desktop/test.MOV" atomically:YES];
这句代码的时候,data是整个文件的完整数据,在文件写入的过程中,data是存在于内存中的,然后一次性写入到本地,如此大的数据存入内存中,当然会出现内存暴增的情况,当写入完成后,系统会自动释放这些内存,所以会出现一个内存峰值。本例中的视频文件只有两百多兆,所以不会出现crash,但是要是下载一个十几个G的文件的时候,不用想,肯定crash!
2.没有下载进度以及暂停/继续
在实际开发中,要下载一个很大的文件,没有下载进度和暂停/继续,基本是不可能顺利的下载下来的,大大影响用户体验!
设置代理实现下载
使用代理实现进度跟进
- 在响应方法中获取到文件总大小。
- 在接收数据的方法中,根据每次接收到的数据长度计算数据的总进度。
设置代理<NSURLConnectionDataDelegate>
//设置请求路径url
NSURL *url = [NSURL URLWithString:@"http://127.0.0.1/videos/IMG_3928.MOV"];
//添加请求request
/*
1. cachePolicy - 缓存策略
- NSURLRequestUseProtocolCachePolicy = 0,
-(常用)默认缓存策略,若使用requestWithURL方法,默认使用该缓存策略;它会根据HTTP头中的信息进行缓存处理,服务器可以在HTTP头中加入Expires和Cache-Control等来告诉客户端应该施行的缓存策略。
- NSURLRequestReloadIgnoringLocalCacheData = 1,
-(偶尔使用)顾名思义,忽略本地缓存,直接加载服务器数据
- NSURLRequestReturnCacheDataElseLoad = 2,
-(不用)一直尝试读取缓存数据,若没有缓存,才会去请求网络,该策略的重大缺陷是无法直到缓存的刷新时机。
- NSURLRequestReturnCacheDataDontLoad = 3,
- (不用)该策略之读取缓存数据,无论何时都不会进行网络请求。
2. timeoutInterval - 超时时间 一般设置在15-30秒 AFNetworking中超时时间默认60s
*/
NSURLRequest *req = [NSURLRequest requestWithURL:url cachePolicy:(NSURLRequestUseProtocolCachePolicy) timeoutInterval:15];
//3.创建连接并设置代理
NSURLConnection *conn = [NSURLConnection connectionWithRequest:req delegate:self];
//4.启动连接
[conn start];
实现NSURLConnectionDataDelegate的几个代理方法
#pragma mark - <NSURLConnectionDataDelegate>
//1.接收服务器的响应 -- 服务器的状态行&响应头 做一些准备工作
/*
NSURLResponse
- expectedContentLength 服务器给的预期数据长度 long long 类型
- suggestedFilename 服务器建议保存的文件名称 NSString 类型
*/
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
self.expectedContentLength = response.expectedContentLength;
self.currentLength = 0;
}
//2.接收服务器的数据 -- 此代理方法可能会调用多次,因为服务器返回数据是将数据拆分成很多段,分段返回给客户端
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
self.currentLength += data.length;
float progress = (float)self.currentLength/self.expectedContentLength;
NSLog(@"下载进度%f",progress);
}
//3.所有数据接收完成 -- 最后的通知
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
NSLog(@"下载完成");
}
//4.下载失败或出现错误
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
NSLog(@"出错了!!!");
}
为实现进度跟进,声明两个变量
/* 文件总大小 */
@property (nonatomic, assign) long long expectedContentLength;
/* 当前已下载的文件大小 */
@property (nonatomic, assign) long long currentLength;
以上代码即可实现下载进度跟进
使用代理实现数据保存
先拼接数据,再写入
这里我们先声明两个变量
/* 保存的目标路径 */
@property (nonatomic, copy) NSString *saveFilePath;
/* 保存的数据 */
@property (nonatomic, strong) NSMutableData *saveData;
接下来,我们在代理方法中实现数据的保存
#pragma mark - <NSURLConnectionDataDelegate>
//1.接收服务器的响应 -- 服务器的状态行&响应头 做一些准备工作
/*
NSURLResponse
- expectedContentLength 服务器给的预期数据长度 long long 类型
- suggestedFilename 服务器建议保存的文件名称 NSString 类型
*/
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
self.expectedContentLength = response.expectedContentLength;
self.currentLength = 0;
//设置保存的目标路径
self.saveFilePath = [@"/Users/jsby-yf007/Desktop" stringByAppendingPathComponent:response.suggestedFilename];
}
- (NSMutableData *)saveData
{
if (!_saveData) {
_saveData = [[NSMutableData alloc] init];
}
return _saveData;
}
//2.接收服务器的数据 -- 此代理方法可能会调用多次,因为服务器返回数据是将数据拆分成很多段,分段返回给客户端
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
self.currentLength += data.length;
float progress = (float)self.currentLength/self.expectedContentLength;
NSLog(@"下载进度%f",progress);
//将获取到的数据拼接
[self.saveData appendData:data];
}
//3.所有数据接收完成 -- 最后的通知
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
//将数据写入磁盘
[self.saveData writeToFile:self.saveFilePath atomically:YES];
//由于saveData为strong类型,使用完之后不会立即释放,故,需手动置nil
self.saveData = nil;
NSLog(@"下载完成");
}
//4.下载失败或出现错误
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
NSLog(@"出错了!!!");
}
以上代码实现了数据的保存,但我们发现,内存依然会出现暴增由此我们可以推断,苹果的sendAsynchronousRequest异步方法内部也是通过这种方式来实现文件的保存
边下载,边保存
从上面的代码中我们发现,将数据统一拼接好后再写入依然会出现内存暴增的情况,所以,边下载,变保存不失为一个比较好的办法,因为每段数据的长度比较小,保存完之后,再释放这部分内存,顾不会出现内存暴增的情况!
1. 使用NSFileHandle实现
#pragma mark - <NSURLConnectionDataDelegate>
//1.接收服务器的响应 -- 服务器的状态行&响应头 做一些准备工作
/*
NSURLResponse
- expectedContentLength 服务器给的预期数据长度 long long 类型
- suggestedFilename 服务器建议保存的文件名称 NSString 类型
*/
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
self.expectedContentLength = response.expectedContentLength;
self.currentLength = 0;
//设置保存的目标路径
self.saveFilePath = [@"/Users/jsby-yf007/Desktop" stringByAppendingPathComponent:response.suggestedFilename];
/*
在使用NSFileHandle进行文件保存的时候,若文件已存在,继续保存的话,数据将继续向后拼接;
因此,这里采用比较粗暴的方式,直接删除已存在的文件(实际开发中不建议这么做)
在实际开发中,我们可以使用NSFileManager对文件进行一系列的判断
*/
[[NSFileManager defaultManager] removeItemAtPath:self.saveFilePath error:nil];
}
//2.接收服务器的数据 -- 此代理方法可能会调用多次,因为服务器返回数据是将数据拆分成很多段,分段返回给客户端
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
self.currentLength += data.length;
float progress = (float)self.currentLength/self.expectedContentLength;
NSLog(@"下载进度%f",progress);
//将获取到的数据拼接
[self writeToFileWithData:data];
}
/*
将数据写入文件 -- 将每段数据按顺序写入文件(拼接数据)
NSFileManager - 文件管理器,主要功能:创建目录,检查目录或文件是否存在,删除目录或文件,遍历目录。。。 主要是针对文件的操作 类似于Mac中的Finder
NSFileHandle - 文件“句柄”(文件指针)对同一文件二进制的读/写操作
这里使用 NSFileHandle 来进行文件的写入
*/
- (void)writeToFileWithData:(NSData *)data
{
/*
NSFileHandle也是对文件指针的操作
注意:当self.saveFilePath目录下的文件不存在时,fileHandleForWritingAtPath方法返回的NSFileHandle对象为nil,因此,我们在使用时,需要进行判断
*/
NSFileHandle *fp = [NSFileHandle fileHandleForWritingAtPath:self.saveFilePath];
//判断文件是否存在,NSFileHandle是对文件的操作,因此,我们先写入一段数据到磁盘
if (fp == nil) {
//如果文件不存,我们先执行写入操作
[data writeToFile:self.saveFilePath atomically:YES];
} else {
//如果文件存在,将data追加到文件的末尾(拼接)
/*
NSFileHandle指针默认指向文件的起始位置,当我们需要追加数据的时候,首先我们需要将文件指针指向文件的末尾,这里,NSFileHandle为我们提供了一个方法seekToEndOfFile,可以将指针移向文件的末尾
*/
[fp seekToEndOfFile];
//写入文件 NSFileHandle 提供了写入文件的方法
[fp writeData:data];
//关闭 -- 在c语言开发中,关于文件的读、写操作,都会涉及到文件的打开和关闭;这里是为了文件数据的安全,同时不关闭打开的文件会占用系统资源
[fp closeFile];
}
}
//3.所有数据接收完成 -- 最后的通知
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
NSLog(@"下载完成");
}
//4.下载失败或出现错误
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
NSLog(@"出错了!!!");
}
以上是使用NSFileHandle来实现的数据写入操作,其中,data只是一个局部变量,使用完即释放,文件是分段写入,则不会出现内存暴增(图4)2. 使用NSOutputStream实现
NSOutputStream文件输出流写入文件的方式是,每段数据会自动向后追加,不需要像NSFileHandle一样操作指针来追加数据
先声明一个文件的输出流对象
/* 文件的输出流 */
@property (nonatomic, strong) NSOutputStream *fileStream;
代理中的实现
#pragma mark - <NSURLConnectionDataDelegate>
//1.接收服务器的响应 -- 服务器的状态行&响应头 做一些准备工作
/*
NSURLResponse
- expectedContentLength 服务器给的预期数据长度 long long 类型
- suggestedFilename 服务器建议保存的文件名称 NSString 类型
*/
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
self.expectedContentLength = response.expectedContentLength;
self.currentLength = 0;
//设置保存的目标路径
self.saveFilePath = [@"/Users/jsby-yf007/Desktop" stringByAppendingPathComponent:response.suggestedFilename];
/*
在使用NSFileHandle进行文件保存的时候,若文件已存在,继续保存的话,数据将继续向后拼接;
因此,这里采用比较粗暴的方式,直接删除已存在的文件(实际开发中不建议这么做)
在实际开发中,我们可以使用NSFileManager对文件进行一系列的判断
*/
[[NSFileManager defaultManager] removeItemAtPath:self.saveFilePath error:nil];
//创建输出流 append(追加)
self.fileStream = [[NSOutputStream alloc] initToFileAtPath:self.saveFilePath append:YES];
//打开输出流
[self.fileStream open];
}
//2.接收服务器的数据 -- 此代理方法可能会调用多次,因为服务器返回数据是将数据拆分成很多段,分段返回给客户端
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
self.currentLength += data.length;
float progress = (float)self.currentLength/self.expectedContentLength;
NSLog(@"下载进度%f",progress);
//将数据追加到文件流中
/*
第一个参数 uint8_t *类型 数据的传输都是通过二进制流的方式传输,uint8_t即8位也就是一个ASCII值,该参数是一个数组类型,NSData提供了一个属性bytes
第二个参数 即数据长度
*/
[self.fileStream write:data.bytes maxLength:data.length];
}
//3.所有数据接收完成 -- 最后的通知
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
// 数据流写入完毕后,关闭输出流
[self.fileStream close];
NSLog(@"下载完成");
}
//4.下载失败或出现错误
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
NSLog(@"出错了!!!");
}
NSURLConnection在多线程下的问题
在我们使用NSURLConnection的异步方法时,下载小文件没有问题,当我们下载大文件时,出现了内存暴增的问题,为解决此问题,我们使用了NSURLConnection的代理方法,但是我们在使用代理方法时,缺忽略了线程问题,那么接下来,问题来了,我们知道,NSURLConnection的代理默认是在主线程中执行的,但是,为了不阻塞UI,我们需要将执行放在子线程上,查看NSURLConnection的方法,我们发现,NSURLConnection提供了一个方法
- (void)setDelegateQueue:(nullable NSOperationQueue*) queue
灵机一动,我们可以在创建连接的之后,开始连接之前来设置一下DelegateQueue将其放入新建队列中也就是子线程中
[conn setDelegateQueue:[[NSOperationQueue alloc] init]];
但是!!!经过测试,我们发现,问题依然存在,测试发现,在下载过程中,UI的操作会阻塞下载
查看connectionWithRequest:方法的注释发现这么一句话
For the connection to work correctly, the calling thread’s run loop must be operating in the default run loop mode.
翻译:为了使连接正常工作,调用线程的runloop必须在默认runloop模式下运行
也就是说,我们创建NSURLConnection连接是在哪个模式下运行,下载任务就在哪个线程
setDelegateQueue这个方法只是将代理方法中的任务放入了子线程中执行,下载任务仍然在主线程中!
接下来要如何解决这个问题呢???
我们首先想到的是,将整个下载任务放在子线程中
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"开始了");
//1.设置请求路径url
NSURL *url = [NSURL URLWithString:@"http://127.0.0.1/videos/IMG_3928.MOV"];
//2.创建请求
NSURLRequest *req = [NSURLRequest requestWithURL:url cachePolicy:(NSURLRequestUseProtocolCachePolicy) timeoutInterval:15];
//3.创建连接并设置代理
NSURLConnection *conn = [NSURLConnection connectionWithRequest:req delegate:self];
//将代理任务放在子线程中执行
[conn setDelegateQueue:[[NSOperationQueue alloc] init]];
//4.启动连接
[conn start];
NSLog(@"结束了");
});
执行完上面的语句后,我们会发现,下载任务根本没执行;这个问题涉及到了一个知识点runloop!每个线程都有一个实际已经存在的runloop(运行循环)。但是,子线程的runloop默认不开启!
那解决方案出来了,我们可以手动来开启。这里使用coreFoundation框架CFRunLoopRef
1.首先声明一个CFRunLoopRef
/* 下载所在线程的runloop */
@property (nonatomic, assign) CFRunLoopRef downloadRunLoop;
2.启动runloop
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"开始了");
//1.设置请求路径url
NSURL *url = [NSURL URLWithString:@"http://127.0.0.1/videos/IMG_3928.MOV"];
//2.创建请求
NSURLRequest *req = [NSURLRequest requestWithURL:url cachePolicy:(NSURLRequestUseProtocolCachePolicy) timeoutInterval:15];
//3.创建连接并设置代理
NSURLConnection *conn = [NSURLConnection connectionWithRequest:req delegate:self];
//将代理任务放在子线程中执行
[conn setDelegateQueue:[[NSOperationQueue alloc] init]];
//4.启动连接
[conn start];
//5.启动runloop
/*
使用coreFoundation框架 中的 CFRunLoopRef
其中有三个我们需要用到的方法
CFRunLoopRun 启动当前线程的runloop
CFRunLoopStop 停止指定线程的runloop
CFRunLoopGetCurrent 拿到当前线程的runloop
*/
//1.拿到当前线程的runloop
self.downloadRunLoop = CFRunLoopGetCurrent();
//2.启动runloop
CFRunLoopRun();
NSLog(@"结束了");
});
3.停止runloop
//3.所有数据接收完成 -- 最后的通知
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
// 数据流写入完毕后,关闭输出流
[self.fileStream close];
//停止下载线程所在的runloop
CFRunLoopStop(self.downloadRunLoop);
NSLog(@"下载完成");
}
到此,使用NSURLConnection实现下载已经基本实现。
本篇文章旨在学习NSURLConnection的原理,如有任何疑问或写的有问题的地方,欢迎大家留言,共同进步!下载中的断点续传功能将在下一篇关于NSURLSession的文章中进行详细描述。