美文网首页iOS实战iOS-开发iOS Developer
iOS 网络文件下载之NSURLConnection

iOS 网络文件下载之NSURLConnection

作者: 小黑_Coder | 来源:发表于2016-08-31 14:30 被阅读272次

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

    [相关代码]https://github.com/LHCoder2016/LHNSURLConnectionDownload.git
    [欢迎讨论] huliuworld@yahoo.com

    相关文章

      网友评论

        本文标题:iOS 网络文件下载之NSURLConnection

        本文链接:https://www.haomeiwen.com/subject/uwgqettx.html