美文网首页
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