美文网首页
NSURLConnection详解延伸之主线程、子线程与

NSURLConnection详解延伸之主线程、子线程与

作者: 952625a28d0d | 来源:发表于2017-02-17 22:09 被阅读120次

    NSURLConnection详解延伸之主线程、子线程与Runloop之间的关系

    • NSURLConnection从iOS 2.0开始
    • 异步加载在iOS 5.0才有,在5.0以前,是通过代理来实现网络开发
    • 开发简单的网络请求还是比较方便的,可以直接用异步方法
    • 开发复杂的网络请求,步骤非常繁琐!!
    /**
     问题:
     1.没有下载进度
     2.内存过大,影响用户体验,有一个最大峰值(是因为NSData一次性写入造成的)
     */
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        // 1. url
        NSString *urlStr = @"http://www.keepvid.com";
        urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
        NSURL *url = [NSURL URLWithString:urlStr];
        NSURLRequest *request = [NSURLRequest requestWithURL:url];
        NSLog(@"开始下载");
    //    [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
    //       
    //        // 数据写入磁盘 data首先是在内存里面 然后一次性写入到磁盘
    //        [data writeToFile:@"/Users/mac/Desktop/123.wmv" atomically:YES];
    //        NSLog(@"完成");
    //        
    //    }];
    }```
    
    ###挖掘NSURLConnection的底层实现
    客户端->封装好一个请求发给服务器-》服务器拿到请求之后-》响应返回状态行&响应头-》数据返回(以二进制的方式进行数据传输)
    
    

    /**
    问题:
    1.没有下载进度
    2.内存过大,影响用户体验,有一个最大峰值(是因为NSData一次性写入造成的)
    */

    • (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
      // 1. url
      NSString *urlStr = @"http://www.keepvid.com";
      urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
      NSURL *url = [NSURL URLWithString:urlStr];
      NSURLRequest *request = [NSURLRequest requestWithURL:url];
      NSLog(@"开始下载");
      // [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
      //
      // // 数据写入磁盘 data首先是在内存里面 然后一次性写入到磁盘
      // [data writeToFile:@"/Users/mac/Desktop/123.wmv" atomically:YES];
      // NSLog(@"完成");
      //
      // }];

      NSURLConnection *conn = [NSURLConnection connectionWithRequest:request delegate:self];
      // 启动一个连接
      [conn start];

    }```

    上代码

    #import "ViewController.h"
    
    /**
     NSURLConnectionDownloadDelegate 千万不要用!!!因为是专门针对杂志的下载提供的借口
     如果在开发中使用DownloadDelegate 下载,能够监听到下载进度,但是无法拿到下载的文件
     Newsstand Kit's 专门用来做杂志
     所以我们一般用NSURLConnectionDataDelegate
     */
    @interface ViewController () <NSURLConnectionDataDelegate>
    
    
    /**
     文件的总长度
     */
    @property (nonatomic, assign) long long expectedContentLength;
    
    
    /**
     当前下载的文件的长度
     */
    @property (nonatomic, assign) long long currentLength;
    
    /**
     保存目标
     */
    @property (nonatomic, copy) NSString * targetFilePath;
    
    /**
     用来每次接收到的数据拼接
     */
    //@property (nonatomic, strong) NSMutableData * fileData;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
    }
    
    /**
     问题:
     1.没有下载进度
     解决办法:
        - 通过代理方式来解决!!
        1.进度跟进!
             - 在我们第一次得到服务器响应的时候获得文件的总大小
             - 每次接收到数据,我们就计算数据的总比例 接收到的数据大小/总数据大小*100
        2.保存文件的思路?
             - 第一种:保存完成再写入磁盘
             测试结果:和我们异步执行的效果是一样的,仍然存在内存问题!
             推测:苹果的异步方法的实现思路:就是我们刚才我们的实现思路
             - 第二种:边下载边写入磁盘
             开始下载,我们每次接受一段Data,就保存在磁盘中,然后释放内存,然后再次接收到新的Data,我们再次存入磁盘并且拼接到之前已经写入的路径中,然后释放内存。
             1.NSFileHandle 彻底解决了内存峰值的问题
     
     2.内存过大,影响用户体验,有一个最大峰值(是因为NSData一次性写入造成的)
    
     */
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        // 1. url
        NSString *urlStr = @"http://www.keepvid.com";
        urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
        NSURL *url = [NSURL URLWithString:urlStr];
        NSURLRequest *request = [NSURLRequest requestWithURL:url];
        NSLog(@"开始下载");
    //    [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
    //       
    //        // 数据写入磁盘 data首先是在内存里面 然后一次性写入到磁盘
    //        [data writeToFile:@"/Users/mac/Desktop/123.wmv" atomically:YES];
    //        NSLog(@"完成");
    //        
    //    }];
        
        NSURLConnection *conn = [NSURLConnection connectionWithRequest:request delegate:self];
        // 启动一个连接
        [conn start];
        
    }
    
    - (void)didReceiveMemoryWarning {
        [super didReceiveMemoryWarning];
        // Dispose of any resources that can be recreated.
    }
    
    
    #pragma mark -- <NSURLConnectionDataDelegate>
    
    // 接收到服务器的响应 - 状态行&响应头 - 做一些准备工作
    // expectedContentLength 需要下载的文件的总大小 long long
    // suggestedFileName     服务器建议保存的文件名称
    - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
        
        NSLog(@"%@",response);
        
        // 记录文件总大小
        self.expectedContentLength = response.expectedContentLength;
        
        // 当前长度刚开始设置为0
        self.currentLength = 0;
        
        // 生成目标文件的路径 建议拼接建议的文件名称
        self.targetFilePath = [@"/Users/mac/Desktop/123" stringByAppendingString:response.suggestedFilename];
        
        // 拿到目标 直接删除 重新下载 防止重复下载 如果文件存在 就会直接删除 如果文件不存在 就什么都不做 也不会报错 内部帮我们处理了
        [[NSFileManager defaultManager] removeItemAtPath:self.targetFilePath error:NULL];
    }
    
    //- (NSMutableData *)fileData{
    //    if (!_fileData) {
    //        _fileData = [[NSMutableData alloc] init];
    //    }
    //    return _fileData;
    //}
    
    // 接收到服务器的数据 - 此代理方法可能会执行很多次 因为我们会接受到多个数据块 因为拿到了多个Data 最终我们对这些Data进行拼接
    - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
        
        NSLog(@"接收到的数据的长度为%tu",data.length);
        
        // 计算当前长度
        self.currentLength += data.length;
        
        // 计算百分比
        // progress = long long / long long 需要强转
        float progerss = (float)self.currentLength / self.expectedContentLength;
        NSLog(@"%f",progerss);
        
        // 拼接数据
    //    [self.fileData appendData:data];
        
        // 写入数据到磁盘
        [self writeToFileWithData:data];
    }
    
    
    /**
     文件写入
    
     @param data 要写入的文件
     */
    - (void)writeToFileWithData:(NSData *)data{
        /**
         NSFileManager:主要功能:创建目录,检查目录是否存在,遍历目录,删除文件。。针对文件操作!!Finder
         NSFileHandle: 来写入我们的文件 文件句柄(管理器、处理、文件指针)Handle 意味着是对前面单词的"File"操作
         主要功能,就是对同一个二进制文件的读和写!
         */
        // 注意:p 是指 指针!如果文件不存在,fp在实例化结果是空
        NSFileHandle *fp = [NSFileHandle fileHandleForReadingAtPath:self.targetFilePath];
        
        // 判断文件是否存在 - 如果文件不存在,直接将数据写入磁盘
        if (fp == nil) {
            // 说明是第一次下载 直接写入磁盘 这里fp就存在了
            [data writeToFile:self.targetFilePath atomically:YES];
        }else{
            // 1:如果文件存在,将文件指针指向文件的末尾
            [fp seekToEndOfFile];
            // [fp seekToFileOffset:(unsigned long long)]; 将指针指向文件的任意方向
            // 2: 写入文件
            [fp writeData:data];
            // 3: 关闭文件,在C语言的开发中,凡是涉及到文件的读写,都会涉及到文件的打开和关闭的操作!!
            [fp closeFile];
        }
    }
    
    // 所有的数据接收完毕 - 这个方法只是一个最后的通知
    - (void)connectionDidFinishLoading:(NSURLConnection *)connection{
        
        NSLog(@"完毕");
        
    //    // 将数据一次性写入磁盘 (和我们刚刚用异步的方式达到的效果是一样的)
    //    [self.fileData writeToFile:self.targetFilePath atomically:YES];
    //    
    //    // 释放我们的fileData 因为我们fileData是strong类型的
    //    self.fileData = nil;
    }
    
    // 下载失败
    - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
        
    }
    
    @end
    
    • 第三种方式 使用NSOutputStream实现
    #import "ViewController.h"
    
    /**
     NSURLConnectionDownloadDelegate 千万不要用!!!因为是专门针对杂志的下载提供的借口
     如果在开发中使用DownloadDelegate 下载,能够监听到下载进度,但是无法拿到下载的文件
     Newsstand Kit's 专门用来做杂志
     所以我们一般用NSURLConnectionDataDelegate
     */
    @interface ViewController () <NSURLConnectionDataDelegate>
    
    
    /**
     文件的总长度
     */
    @property (nonatomic, assign) long long expectedContentLength;
    
    
    /**
     当前下载的文件的长度
     */
    @property (nonatomic, assign) long long currentLength;
    
    /**
     保存目标
     */
    @property (nonatomic, copy) NSString * targetFilePath;
    
    /**
     用来每次接收到的数据拼接
     */
    //@property (nonatomic, strong) NSMutableData * fileData;
    
    @property (nonatomic, strong) NSOutputStream *outputStream;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
    }
    
    /**
     问题:
     1.没有下载进度
     解决办法:
        - 通过代理方式来解决!!
        1.进度跟进!
             - 在我们第一次得到服务器响应的时候获得文件的总大小
             - 每次接收到数据,我们就计算数据的总比例 接收到的数据大小/总数据大小*100
        2.保存文件的思路?
             - 第一种:保存完成再写入磁盘
             测试结果:和我们异步执行的效果是一样的,仍然存在内存问题!
             推测:苹果的异步方法的实现思路:就是我们刚才我们的实现思路
             - 第二种:边下载边写入磁盘
             开始下载,我们每次接受一段Data,就保存在磁盘中,然后释放内存,然后再次接收到新的Data,我们再次存入磁盘并且拼接到之前已经写入的路径中,然后释放内存。
             1.NSFileHandle 彻底解决了内存峰值的问题
     
     2.内存过大,影响用户体验,有一个最大峰值(是因为NSData一次性写入造成的)
    
     */
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        // 1. url
        NSString *urlStr = @"http://www.keepvid.com";
        urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
        NSURL *url = [NSURL URLWithString:urlStr];
        NSURLRequest *request = [NSURLRequest requestWithURL:url];
        NSLog(@"开始下载");
    //    [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
    //       
    //        // 数据写入磁盘 data首先是在内存里面 然后一次性写入到磁盘
    //        [data writeToFile:@"/Users/mac/Desktop/123.wmv" atomically:YES];
    //        NSLog(@"完成");
    //        
    //    }];
        
        NSURLConnection *conn = [NSURLConnection connectionWithRequest:request delegate:self];
        // 启动一个连接
        [conn start];
        
    }
    
    - (void)didReceiveMemoryWarning {
        [super didReceiveMemoryWarning];
        // Dispose of any resources that can be recreated.
    }
    
    
    #pragma mark -- <NSURLConnectionDataDelegate>
    
    // 接收到服务器的响应 - 状态行&响应头 - 做一些准备工作
    // expectedContentLength 需要下载的文件的总大小 long long
    // suggestedFileName     服务器建议保存的文件名称
    - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
        
        NSLog(@"%@",response);
        
        // 记录文件总大小
        self.expectedContentLength = response.expectedContentLength;
        
        // 当前长度刚开始设置为0
        self.currentLength = 0;
        
        // 生成目标文件的路径 建议拼接建议的文件名称
        self.targetFilePath = [@"/Users/mac/Desktop/123" stringByAppendingString:response.suggestedFilename];
        
        // 拿到目标 直接删除 重新下载 防止重复下载 如果文件存在 就会直接删除 如果文件不存在 就什么都不做 也不会报错 内部帮我们处理了
        [[NSFileManager defaultManager] removeItemAtPath:self.targetFilePath error:NULL];
        
        // 第二种方式 以拼接的方式打开数据流
        self.outputStream = [[NSOutputStream alloc] initToFileAtPath:self.targetFilePath append:YES];
        [self.outputStream open];
    }
    
    //- (NSMutableData *)fileData{
    //    if (!_fileData) {
    //        _fileData = [[NSMutableData alloc] init];
    //    }
    //    return _fileData;
    //}
    
    // 接收到服务器的数据 - 此代理方法可能会执行很多次 因为我们会接受到多个数据块 因为拿到了多个Data 最终我们对这些Data进行拼接
    - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
        
        NSLog(@"接收到的数据的长度为%tu",data.length);
        
        // 计算当前长度
        self.currentLength += data.length;
        
        // 计算百分比
        // progress = long long / long long 需要强转
        float progerss = (float)self.currentLength / self.expectedContentLength;
        NSLog(@"%f",progerss);
        
        // 拼接数据
    //    [self.fileData appendData:data];
        
        // 写入数据到磁盘
        [self writeToFileWithData:data];
        
        // 将数据追加到数据流
        [self.outputStream write:data.bytes maxLength:data.length];
    }
    
    
    /**
     文件写入
    
     @param data 要写入的文件
     */
    - (void)writeToFileWithData:(NSData *)data{
        /**
         NSFileManager:主要功能:创建目录,检查目录是否存在,遍历目录,删除文件。。针对文件操作!!Finder
         NSFileHandle: 来写入我们的文件 文件句柄(管理器、处理、文件指针)Handle 意味着是对前面单词的"File"操作
         主要功能,就是对同一个二进制文件的读和写!
         */
        // 注意:p 是指 指针!如果文件不存在,fp在实例化结果是空
        NSFileHandle *fp = [NSFileHandle fileHandleForReadingAtPath:self.targetFilePath];
        
        // 判断文件是否存在 - 如果文件不存在,直接将数据写入磁盘
        if (fp == nil) {
            // 说明是第一次下载 直接写入磁盘 这里fp就存在了
            [data writeToFile:self.targetFilePath atomically:YES];
        }else{
            // 1:如果文件存在,将文件指针指向文件的末尾
            [fp seekToEndOfFile];
            // [fp seekToFileOffset:(unsigned long long)]; 将指针指向文件的任意方向
            // 2: 写入文件
            [fp writeData:data];
            // 3: 关闭文件,在C语言的开发中,凡是涉及到文件的读写,都会涉及到文件的打开和关闭的操作!!
            [fp closeFile];
        }
    }
    
    // 所有的数据接收完毕 - 这个方法只是一个最后的通知
    - (void)connectionDidFinishLoading:(NSURLConnection *)connection{
        
        NSLog(@"完毕");
        
    //    // 将数据一次性写入磁盘 (和我们刚刚用异步的方式达到的效果是一样的)
    //    [self.fileData writeToFile:self.targetFilePath atomically:YES];
    //    
    //    // 释放我们的fileData 因为我们fileData是strong类型的
    //    self.fileData = nil;
        
        // 关闭数据流
        [self.outputStream close];
    }
    
    // 下载失败
    - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
        
    }
    
    @end
    

    以上的代码都是在主线程中操作 现在我们以多线程处理一下,因为主线程会卡顿UI

    /**
         For the connection to work correctly, the calling thread’s run loop must be operating in the default run loop mode.
         为了保证我们的正常工作,调用线程的Runloop必须运行在默认的循环模式下!!(这是苹果的解释)
         所以为什么不用Connection?因为Connection大数据下载的时候,使用代理的话,多线程的话就会有问题。
         */
        NSURLConnection *conn = [NSURLConnection connectionWithRequest:request delegate:self];
        
        // 设置代理工作的操作队列,即让它在异步线程中去做 但是这样做UI主线程的事件仍然会阻塞我们的下载工作,为什么呢?怎么办呢?
        [conn setDelegateQueue:[[NSOperationQueue alloc] init]];
         
        // 启动一个连接
        [conn start];
    

    Connection在多线程中的问题

    • 因为我们创建的方式是在主线程,那么无论我们怎么做,下载都会在主线程。

    解决方式:

    我们连初始化都放在子线程中做
    但是不能解决,使用这个方式,整个网络都不会走
    为什么呢?
    线程死了。。。

    主线程和子线程在运行上有很大的区别?

    • 同样是从上到下执行任务,但是有区别。
    • 主线程和子线程都有Runloop,但是主线程的Runloop和子线程的有什么区别?
      子线程中的Runloop是不启动的,那么我们的运行循环是来干什么的?
      Runloop是来负责监听滑动、触摸、时钟、和网络事件。主线程一启动,Runloop是启动的,但是我们一旦在子线程创建的网络事件,Runloop是不启动的,那么我们在子线程中的代码执行完毕,线程就被回收了,因为Runloop没有启动。

    那么如何来解决这个问题呢?

    我们在启动连接之后,我们要启动运行循环!!!!!

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        
        // 将下载加入子线程
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            
            // 1. url
            NSString *urlStr = @"http://www.keepvid.com";
            urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
            NSURL *url = [NSURL URLWithString:urlStr];
            NSURLRequest *request = [NSURLRequest requestWithURL:url];
            NSLog(@"开始下载");
            //    [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
            //
            //        // 数据写入磁盘 data首先是在内存里面 然后一次性写入到磁盘
            //        [data writeToFile:@"/Users/mac/Desktop/123.wmv" atomically:YES];
            //        NSLog(@"完成");
            //
            //    }];
            
            /**
             For the connection to work correctly, the calling thread’s run loop must be operating in the default run loop mode.
             为了保证我们的正常工作,调用线程的Runloop必须运行在默认的循环模式下!!(这是苹果的解释)
             所以为什么不用Connection?因为Connection大数据下载的时候,使用代理的话,多线程的话就会有问题。
             */
            NSURLConnection *conn = [NSURLConnection connectionWithRequest:request delegate:self];
            
            // 设置代理工作的操作队列,即让它在异步线程中去做 但是这样做UI主线程的事件仍然会阻塞我们的下载工作,为什么呢?怎么办呢?
            [conn setDelegateQueue:[[NSOperationQueue alloc] init]];
            
            // 启动一个连接
            [conn start];
            
            NSLog(@"来了");
            
            // 启动运行循环
            [[NSRunLoop currentRunLoop] run];
        });
    }
    

    这样的话 我们解决了这个问题

    但是,我们下载结束之后,Runloop和线程没有被杀死,是很耗费内存的。

    • 所以 我们不要使用Run的方式来启动,我们应该使用另一种方式,即手动的方式来启动。
    self.isFinished = NO;   // 先设置为NO
            
            while (!self.isFinished) {
                // 启动一次Runloop的循环,监听事件 从现在开始监听0.1秒的Runloop事件
                [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
            }
    
    // 下载完毕 设置我们的结束标记 这样的话 我们就停掉了Runloop
        self.isFinished = YES;```
    
    ####但是这样的方式,对系统的消耗还是非常大的。那么我们要怎么做呢?
    
    我们不用NSRunloop 我们用CFRunloop,就可以自己来管理当前Runloop循环了
    
    - 创建一个运行循环属性来获得当前Runloop
    
    

    /**
    下载线程的运行循环
    */
    @property (nonatomic, assign) CFRunLoopRef downloadRunloop;```

    • 在子线程中下载的时候赋值,并开启Runloop
     // 1.拿到当前线程的运行循环
            self.downloadRunloop = CFRunLoopGetCurrent();
            // 2.启动运行循环
            CFRunLoopRun();```
    
    - 下载结束 关闭Runloop 停掉当前的运行循环
    
    

    // 停止下载线程所在的运行循环
    CFRunLoopStop(self.downloadRunloop);```

    完整代码

    //
    //  ViewController.m
    //  NSURLConnection
    //
    //  Created by mac on 2017/2/17.
    //  Copyright © 2017年 mac. All rights reserved.
    //
    
    #import "ViewController.h"
    
    /**
     NSURLConnectionDownloadDelegate 千万不要用!!!因为是专门针对杂志的下载提供的借口
     如果在开发中使用DownloadDelegate 下载,能够监听到下载进度,但是无法拿到下载的文件
     Newsstand Kit's 专门用来做杂志
     所以我们一般用NSURLConnectionDataDelegate
     */
    @interface ViewController () <NSURLConnectionDataDelegate>
    
    
    /**
     文件的总长度
     */
    @property (nonatomic, assign) long long expectedContentLength;
    
    
    /**
     当前下载的文件的长度
     */
    @property (nonatomic, assign) long long currentLength;
    
    /**
     保存目标
     */
    @property (nonatomic, copy) NSString * targetFilePath;
    
    /**
     用来每次接收到的数据拼接
     */
    //@property (nonatomic, strong) NSMutableData * fileData;
    
    
    /**
     接收流数据
     */
    @property (nonatomic, strong) NSOutputStream *outputStream;
    
    @property (nonatomic, assign) BOOL isFinished;
    
    
    /**
     下载线程的运行循环
     */
    @property (nonatomic, assign) CFRunLoopRef downloadRunloop;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
    }
    
    /**
     问题:
     1.没有下载进度
     解决办法:
        - 通过代理方式来解决!!
        1.进度跟进!
             - 在我们第一次得到服务器响应的时候获得文件的总大小
             - 每次接收到数据,我们就计算数据的总比例 接收到的数据大小/总数据大小*100
        2.保存文件的思路?
             - 第一种:保存完成再写入磁盘
             测试结果:和我们异步执行的效果是一样的,仍然存在内存问题!
             推测:苹果的异步方法的实现思路:就是我们刚才我们的实现思路
             - 第二种:边下载边写入磁盘
             开始下载,我们每次接受一段Data,就保存在磁盘中,然后释放内存,然后再次接收到新的Data,我们再次存入磁盘并且拼接到之前已经写入的路径中,然后释放内存。
             1.NSFileHandle 彻底解决了内存峰值的问题
     
     2.内存过大,影响用户体验,有一个最大峰值(是因为NSData一次性写入造成的)
     
     新的问题:
         默认的Connection是在主线程工作,指定了代理的工作队列之后,整个下载仍然是在主线程!!UI事件阻塞了下载事件
    
     */
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        
        // 将下载加入子线程
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            
            // 1. url
            NSString *urlStr = @"http://www.keepvid.com";
            urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
            NSURL *url = [NSURL URLWithString:urlStr];
            NSURLRequest *request = [NSURLRequest requestWithURL:url];
            NSLog(@"开始下载");
            //    [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
            //
            //        // 数据写入磁盘 data首先是在内存里面 然后一次性写入到磁盘
            //        [data writeToFile:@"/Users/mac/Desktop/123.wmv" atomically:YES];
            //        NSLog(@"完成");
            //
            //    }];
            
            /**
             For the connection to work correctly, the calling thread’s run loop must be operating in the default run loop mode.
             为了保证我们的正常工作,调用线程的Runloop必须运行在默认的循环模式下!!(这是苹果的解释)
             所以为什么不用Connection?因为Connection大数据下载的时候,使用代理的话,多线程的话就会有问题。
             */
            NSURLConnection *conn = [NSURLConnection connectionWithRequest:request delegate:self];
            
            // 设置代理工作的操作队列,即让它在异步线程中去做 但是这样做UI主线程的事件仍然会阻塞我们的下载工作,为什么呢?怎么办呢?
            [conn setDelegateQueue:[[NSOperationQueue alloc] init]];
            
            // 启动一个连接
            [conn start];
            
    //        self.isFinished = NO;   // 先设置为NO
    //        
    //        while (!self.isFinished) {
    //            // 启动一次Runloop的循环,监听事件 从现在开始监听0.1秒的Runloop事件
    //            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
    //        }
    //        
    //        NSLog(@"来了");
    //        
    //        // 启动运行循环
    //        [[NSRunLoop currentRunLoop] run];
            
            // CoreFoundation 框架 CFRunloop
            /*
             CFRunloopStop() 停止执行的Runloop
             CFRunLoopGetCurrent() 拿到当前的Runloop
             CFRunLoopRun(); 直接启动当前的运行循环
             */
            // 1.拿到当前线程的运行循环
            self.downloadRunloop = CFRunLoopGetCurrent();
            // 2.启动运行循环
            CFRunLoopRun();
        });
    }
    
    - (void)didReceiveMemoryWarning {
        [super didReceiveMemoryWarning];
        // Dispose of any resources that can be recreated.
    }
    
    
    #pragma mark -- <NSURLConnectionDataDelegate>
    
    // 接收到服务器的响应 - 状态行&响应头 - 做一些准备工作
    // expectedContentLength 需要下载的文件的总大小 long long
    // suggestedFileName     服务器建议保存的文件名称
    - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
        
        NSLog(@"%@",response);
        
        // 记录文件总大小
        self.expectedContentLength = response.expectedContentLength;
        
        // 当前长度刚开始设置为0
        self.currentLength = 0;
        
        // 生成目标文件的路径 建议拼接建议的文件名称
        self.targetFilePath = [@"/Users/mac/Desktop/123" stringByAppendingString:response.suggestedFilename];
        
        // 拿到目标 直接删除 重新下载 防止重复下载 如果文件存在 就会直接删除 如果文件不存在 就什么都不做 也不会报错 内部帮我们处理了
        [[NSFileManager defaultManager] removeItemAtPath:self.targetFilePath error:NULL];
        
        // 第二种方式 以拼接的方式打开数据流
        self.outputStream = [[NSOutputStream alloc] initToFileAtPath:self.targetFilePath append:YES];
        [self.outputStream open];
    }
    
    //- (NSMutableData *)fileData{
    //    if (!_fileData) {
    //        _fileData = [[NSMutableData alloc] init];
    //    }
    //    return _fileData;
    //}
    
    // 接收到服务器的数据 - 此代理方法可能会执行很多次 因为我们会接受到多个数据块 因为拿到了多个Data 最终我们对这些Data进行拼接
    - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
        
        NSLog(@"接收到的数据的长度为%tu",data.length);
        
        // 计算当前长度
        self.currentLength += data.length;
        
        // 计算百分比
        // progress = long long / long long 需要强转
        float progerss = (float)self.currentLength / self.expectedContentLength;
        NSLog(@"%f",progerss);
        
        // 拼接数据
    //    [self.fileData appendData:data];
        
        // 写入数据到磁盘
        [self writeToFileWithData:data];
        
        // 将数据追加到数据流
        [self.outputStream write:data.bytes maxLength:data.length];
    }
    
    
    /**
     文件写入
    
     @param data 要写入的文件
     */
    - (void)writeToFileWithData:(NSData *)data{
        /**
         NSFileManager:主要功能:创建目录,检查目录是否存在,遍历目录,删除文件。。针对文件操作!!Finder
         NSFileHandle: 来写入我们的文件 文件句柄(管理器、处理、文件指针)Handle 意味着是对前面单词的"File"操作
         主要功能,就是对同一个二进制文件的读和写!
         */
        // 注意:p 是指 指针!如果文件不存在,fp在实例化结果是空
        NSFileHandle *fp = [NSFileHandle fileHandleForReadingAtPath:self.targetFilePath];
        
        // 判断文件是否存在 - 如果文件不存在,直接将数据写入磁盘
        if (fp == nil) {
            // 说明是第一次下载 直接写入磁盘 这里fp就存在了
            [data writeToFile:self.targetFilePath atomically:YES];
        }else{
            // 1:如果文件存在,将文件指针指向文件的末尾
            [fp seekToEndOfFile];
            // [fp seekToFileOffset:(unsigned long long)]; 将指针指向文件的任意方向
            // 2: 写入文件
            [fp writeData:data];
            // 3: 关闭文件,在C语言的开发中,凡是涉及到文件的读写,都会涉及到文件的打开和关闭的操作!!
            [fp closeFile];
        }
    }
    
    // 所有的数据接收完毕 - 这个方法只是一个最后的通知
    - (void)connectionDidFinishLoading:(NSURLConnection *)connection{
        
        NSLog(@"完毕");
        
    //    // 将数据一次性写入磁盘 (和我们刚刚用异步的方式达到的效果是一样的)
    //    [self.fileData writeToFile:self.targetFilePath atomically:YES];
    //    
    //    // 释放我们的fileData 因为我们fileData是strong类型的
    //    self.fileData = nil;
        
        // 关闭数据流
        [self.outputStream close];
        
        // 下载完毕 设置我们的结束标记 这样的话 我们就停掉了Runloop
    //    self.isFinished = YES;
        
        // 停止下载线程所在的运行循环
        CFRunLoopStop(self.downloadRunloop);
    }
    
    // 下载失败
    - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
        
    }
    
    @end
    
    @property (nonatomic, assign,getter=isFinished) BOOL finished;
    

    getter=isFinished 指的也就是

    相关文章

      网友评论

          本文标题:NSURLConnection详解延伸之主线程、子线程与

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