美文网首页
从实现下载来认识NSURLConnection

从实现下载来认识NSURLConnection

作者: Luke_Hu | 来源:发表于2018-11-26 10:08 被阅读0次

    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

    上面代码在实际开发中所带来的问题

    1.内存会暴涨,出现一个峰值 图2

    出现图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(@"出错了!!!");
    }
    
    以上代码实现了数据的保存,但我们发现,内存依然会出现暴增 图3

    由此我们可以推断,苹果的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) 图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的文章中进行详细描述。

    相关文章

      网友评论

          本文标题:从实现下载来认识NSURLConnection

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