iOS 网络文件下载之NSURLConnection

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接口,大家都知道pausecancel可是有着天大的区别。那么问题来了,没有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大小,数据会告诉你我们的代码量是值得的
iOS 网络文件下载之NSURLConnection_第1张图片
效果图
  • NSURLConnection的讲解这个告一段落了。但是这个时候有童鞋又该抱怨了,NSURLConnection下载都已经被废弃了为什么还要讲解这个,并且也没有NSURLSession下载好用。那我只能反问这位同学了,学习OC前为什么要C语言,高级语言已经位我们分装好了各种数据结构和算法,为什么我们还要去学习数据结构和算法呢。

[相关代码]https://github.com/LHCoder2016/LHNSURLConnectionDownload.git
[欢迎讨论] [email protected]

你可能感兴趣的:(iOS 网络文件下载之NSURLConnection)