iOS 网络文件下载之NSURLConnection
- 我想不管哪一个app都会设计到下载,小到一张图片,大到一步蓝光高清电影。
- 博客中只添加一些关键的代码,需要完整代码的同学可到个人Github上自行下载,下载地址见博客底部。
小文件的网络下载
NSData下载方式
- 下载一张图片,因为图片比较小,我们直接下载,不用涉及到断点续传的问题,这里就不和大家多说了,直接上代码吧。需要注意的是,将下载的操作放倒子线程里面。
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
imageView.center = self.view.center;
[self.view addSubview:imageView];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"http://attach2.scimg.cn/forum/201503/17/172255yjcdki30xted033j.jpg"]];
dispatch_async(dispatch_get_main_queue(), ^{
imageView.image = [UIImage imageWithData:data];
});
});
NSURLConnection方式下载
- 其实就是发送了了一个异步的Get请求,请求完成回调的Block中的data就是我门需要的图片信息。此方法在iOS 9被废弃了
NSURL* url = [NSURL URLWithString:@"http://attach2.scimg.cn/forum/201503/17/172255yjcdki30xted033j.jpg"];
[NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:url] queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
self.imageView.image = [UIImage imageWithData:data];
}];
上面我们也提到了,因为图片比较小,所以我们不用涉及到断点续传,防止重复下载问题,但是我们如果下载一个蓝光高清呢?这个座位用户当然希望可以断点续存了,好如何处理好下载问题,着就是本篇博客重点讲解的内容
大文件的网络下载
- 对于大文件的下载上面的两种方式都不合适,因为上面两种方式都是一次性回调,下载整个文件放在内存中,如果文件过大,内存就回暴涨。因此
NSURLConnection
还提供了另一种下载方式。
NSURLConnection方式下载
- 好这里就不和大家废话了,还是直接上代码加以说明吧。
//不用多说当然先是初始化一个NSURLConnection的实例了
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://download.xmcdn.com/group18/M01/BC/91/wKgJKlfAEN6wZgwhANQvLrUQ3Pg146.aac"]];
_connection = [NSURLConnection connectionWithRequest:request delegate:self];
- 很显然我们在初始化
NSURLConnection
实例的时候指定了代理,这个时候我们遵守协议了NSURLConnectionDelegate
接下来我们看一下代理方法
//此代理方法在请求接收到服务器响应的时候回调
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
//此代理方法在下载中会多次回调,每次传回一部分数据
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data;
//此代理方法在下载完成的时候回调
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
-
因为
NSURLConnection
下载方式是每次传回一部分数据,因此通常的做法是定义一个NSMutableData
的属性downloadMData
,然后在- (void)connection:(NSURLConnection *)connection didWriteData:(long long)bytesWritten totalBytesWritten:(long long)totalBytesWritten expectedTotalBytes:(long long) expectedTotalBytes
此代理方法中将每次返回的数据拼接在downloadMData
后面,最后在下载完成的回调中方法- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
中将downloadMData
写入sandbox
中。 -
这个时候可能就有同学有疑问了,这样下载不是依然要将下载的全部文件存在了内存中吗?如果文件过大还是会造成内存暴涨。没错,这样确实依然会造成内存暴涨的问题。既然上述方法依然会带来内存暴涨的问题在里就不掩饰了,好,那么我们下面就来重点解决这个内存暴涨的问题,依然和上面一样,废话不多说直接用代码来解决问题。
//首先我们是设置相关的属性
@property (nonatomic, strong) NSURLConnection *connection; //NSURLConnection下载实例
@property (nonatomic, strong) UIView *progressView; //下载进度条
@property (nonatomic, strong) UILabel *progressLabel; //显示进度百分比
@property (nonatomic, strong) NSMutableData *downloadData; //下载的数据
@property (nonatomic, strong) NSFileHandle *fileHandle; //用来操作数据的句柄
@property (nonatomic, assign) long long writtenDataLength; //累计写入长度
@property (nonatomic, assign) long long totalwriteDataLength; //共需要写入的长度
//收到服务器的响应时
//1.创建一个空的文件夹用来存储下载的文件
//2.并初始化用来操作数据的句柄
//3.记录需要文件的总长度
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
NSString *cachePath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).lastObject;
NSString *filePath = [cachePath stringByAppendingPathComponent:response.suggestedFilename];
NSFileManager *fileManager = [NSFileManager defaultManager];
[fileManager createFileAtPath:filePath contents:nil attributes:nil];
_fileHandle = [NSFileHandle fileHandleForWritingAtPath:filePath];
_writtenDataLength = 0;
_totalwriteDataLength = response.expectedContentLength;
}
//收到服务器返回的数据
//1.将句柄移到文件的最尾端
//2.将此次返回的数据写入sandbox
//3.更新已经下载的长度
//4.刷新UI
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[_fileHandle seekToEndOfFile];
[_fileHandle writeData:data];
_writtenDataLength += data.length;
_progressLabel.text = [NSString stringWithFormat:@"%.2f%%", (double)_writtenDataLength/(double)_totalwriteDataLength*100];
_progressView.frame = CGRectMake(0, 0, ([UIScreen mainScreen].bounds.size.width-200)*_writtenDataLength/_totalwriteDataLength, 1);
}
//下载完成
//1.关闭文件夹
//2.销毁操作数据的句柄
//3.清空数据
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
[_fileHandle closeFile];
_fileHandle = nil;
_writtenDataLength = 0;
_totalwriteDataLength = 0;
}
- 这个时候有同学就又要问了,
NSURLConnection
没有提供pause
的API接口,只有一个cancel
的API接口,大家都知道pause
和cancel
可是有着天大的区别。那么问题来了,没有pause
我们改怎么做断点续传呢。没有断点续传的下载,那体验就太差了。
NSURLConnection实现断点续传
- 上面已经提到了
NSURLConnection
没有提供pause
的API接口,那我们该如何实现断点下载呢。好废话不多说了,直接看初始化方法+ (nullable NSURLConnection*)connectionWithRequest:(NSURLRequest *)request delegate:(nullable id)delegate
这个时候我们发现需要一个NSURLRequest
的参数,这说明我们可以从HTTP
协议请求头的Range
入手。下面先介绍一下HTTP
协议请求头的Range
。
//Range 可以指定每次从网络下载数据包的大小
bytes = 0 - 499 //从0到499共500
bytes = 500 - //从500到结束
bytes = -500 //最后500
bytes = 500 - 599, 800 - 899 //同时指定几个范围
- OK只要我们初始化的时候设置好
HTTP
协议请求头的Range
就好了,然后在代理方法中稍微做一些逻辑处理
[sennder setTitle:@"Pause Download" forState:UIControlStateNormal];
NSURL *url = [NSURL URLWithString:@"http://download.xmcdn.com/group18/M01/BC/91/wKgJKlfAEN6wZgwhANQvLrUQ3Pg146.aac"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
NSString *range = [NSString stringWithFormat:@"bytes=%lld-", _writtenDataLength];
[request setValue:range forHTTPHeaderField:@"Range"];
_connection = [NSURLConnection connectionWithRequest:request delegate:self];
//收到服务器的响应时
//1.创建一个空的文件夹用来存储下载的文件
//2.并初始化用来操作数据的句柄
//3.记录需要文件的总长度
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
NSString *cachePath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).lastObject;
NSString *filePath = [cachePath stringByAppendingPathComponent:response.suggestedFilename];
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager fileExistsAtPath:filePath])
{
NSData *writtenData = [NSData dataWithContentsOfFile:filePath];
[fileManager createFileAtPath:filePath contents:writtenData attributes:nil];
_writtenDataLength = writtenData.length;
}
_fileHandle = [NSFileHandle fileHandleForWritingAtPath:filePath];
_totalwriteDataLength = response.expectedContentLength + _writtenDataLength;
}
NSURLConnection方式下载充分利用CPU性能
- 为了提升下载效率,我们通常开启3~5个线程同时下载一个文件。然后计算每一段的下载量,分别写入对应的文件部分,当然这就要求我们初始化多个
NSURLConnection
实例。依然是废话不多说了,直接上代码。 - 博客中只添加一些关键的代码,需要完整代码的同学可到个人Github上自行下载,下载地址见博客底部,如果喜欢留下star
#pragma mark - NSURLConnectionDataDelegate
//1.获取要下载的总长度
//2.取消出发下载NSURLConnection
//3.将总长度分给4个NSURLConnection分别去下载
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
if ([connection isEqual:_connection])
{
_totalWriteDataLength = response.expectedContentLength;
[_connection cancel];
NSString *cachePath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).lastObject;
for (int i = 0; i < 4; i++)
{
NSString *filePath = [cachePath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@_%d", response.suggestedFilename, i]];
NSFileManager *fileManager = [NSFileManager defaultManager];
[fileManager createFileAtPath:filePath contents:nil attributes:nil];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://dldir1.qq.com/qqfile/QQforMac/QQ_V5.1.1.dmg"]];
NSString *range = [NSString stringWithFormat:@"bytes=%lld-%lld", response.expectedContentLength/4*i, response.expectedContentLength/4*(i+1)];
[request setValue:range forHTTPHeaderField:@"Range"];
NSURLConnection *connection = [NSURLConnection connectionWithRequest:request delegate:self];
NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath:filePath];
[_pathMArr addObject:filePath];
[_connectionMArr addObject:connection];
[_fileHandleMArr addObject:fileHandle];
}
}
}
//1.将句柄移到文件的最尾端
//2.将此次返回的数据写入sandbox
//3.更新已经下载的长度
//4.刷新UI
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
NSInteger index = [_connectionMArr indexOfObject:connection];
NSFileHandle *fileHandle = [_fileHandleMArr objectAtIndex:index];
[fileHandle seekToEndOfFile];
[fileHandle writeData:data];
switch (index) {
case 0:
{
_writtenDataLength_1 += data.length;
_progressLabel_1.text = [NSString stringWithFormat:@"%.2f%%", (double)_writtenDataLength_1/(double)_totalWriteDataLength*4*100];
break;
}
case 1:
{
_writtenDataLength_2 += data.length;
_progressLabel_2.text = [NSString stringWithFormat:@"%.2f%%", (double)_writtenDataLength_2/(double)_totalWriteDataLength*4*100];
break;
}
case 2:
{
_writtenDataLength_3 += data.length;
_progressLabel_3.text = [NSString stringWithFormat:@"%.2f%%", (double)_writtenDataLength_3/(double)_totalWriteDataLength*4*100];
break;
}
case 3:
{
_writtenDataLength_4 += data.length;
_progressLabel_4.text = [NSString stringWithFormat:@"%.2f%%", (double)_writtenDataLength_4/(double)_totalWriteDataLength*4*100];
break;
}
default:
break;
}
_totalWrittenDataLength = _writtenDataLength_1 + _writtenDataLength_2 + _writtenDataLength_3 + _writtenDataLength_4;
_progressLabel.text = [NSString stringWithFormat:@"%.2f%%", (double)_totalWrittenDataLength/(double)_totalWriteDataLength*100];
}
//1.记录完成任务的个数
//2.判断总任务是否完成
//3.合并文件
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
_finishedCount++;
NSInteger index = [_connectionMArr indexOfObject:connection];
NSFileHandle *fileHandle = [_fileHandleMArr objectAtIndex:index];
[fileHandle closeFile];
fileHandle = nil;
if (_finishedCount == 4)//将4个任务下载的文件合并成一个
{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *tmpPath = [_pathMArr objectAtIndex:index];
NSString *filePath = [tmpPath substringToIndex:tmpPath.length];
[fileManager createDirectoryAtPath:filePath withIntermediateDirectories:YES attributes:nil error:nil];
NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath:filePath];
for (int i = 0; i < 4; i++) {
[fileHandle seekToEndOfFile];
[fileHandle writeData:[NSData dataWithContentsOfFile:[_pathMArr objectAtIndex:i]]];
}
[fileHandle closeFile];
fileHandle = nil;
NSLog(@"%@", filePath);
_progressLabel.text = @"下载完成";
}
}
- 说了这么多我们看看效果,下载的同时大家也注意观察CPU的使用率和Memory大小,数据会告诉你我们的代码量是值得的
- 对
NSURLConnection
的讲解这个告一段落了。但是这个时候有童鞋又该抱怨了,NSURLConnection
下载都已经被废弃了为什么还要讲解这个,并且也没有NSURLSession
下载好用。那我只能反问这位同学了,学习OC前为什么要C语言,高级语言已经位我们分装好了各种数据结构和算法,为什么我们还要去学习数据结构和算法呢。
[相关代码]https://github.com/LHCoder2016/LHNSURLConnectionDownload.git
[欢迎讨论] huliuworld@yahoo.com
网友评论