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 指的也就是
网友评论