NSURLConnection文件下载和NSRunLoopCommonModes

传智播客视频学习笔记+个人总结
发送请求的步骤:
1.设置url
2.设置request
3.发送请求,同步or异步

使用同步方法下载文件

在主线程调用同步方法,一直在等待服务器返回数据,代码会卡住,如果服务器,没有返回数据,那么在主线程UI会卡住不能继续执行操作。有返回值NSData

//1.url
NSString *urlstr = @"xxxx";
urlstr = [urlstr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL *url = [NSURL URLWithString:urlstr];
//2.request
NSURLRequest *request = [NSURLRequest requestWithURL:url];
//3.connection
NSLog(@"start");
NSData *data=[NSURLConnection sendSynchronousRequest:request returningResponse:nil error:nil];
//一直在等待服务器返回数据
NSLog(@"--%d--",data.length);
  • 注意:同步的连接会阻塞调用它的线程,不一定是主线程

使用异步方法下载文件

没有返回值

    //1.url
    NSString *urlstr = @"xxxx";
    urlstr = [urlstr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    NSURL *url = [NSURL URLWithString:urlstr];
    
    //2.request
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    
    //3.connection
    NSLog(@"start");
    //使用这个方法会有内存峰值
    //queue参数是指定block的执行队列
    [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
        //将文件写入磁盘
        //内存峰值:下载完成瞬间再把文件写入磁盘。下载文件有多大,NSData就会占用多大的内存。
        [data writeToFile:@"XXXXXX" atomically:YES];
        NSLog(@"finsh");
    }];
  • 开一条新的线程去发送请求,主线程继续往下走,当拿到服务器的返回数据的数据的时候再回调block,执行block代码段。不会卡住主线程。
  • 关于queue参数,队列的作用:An NSOperationQueue upon which the handler block will be dispatched.决定这个block操作放在哪个线程执行。
    如果是NSOperationQueue *queue=[[NSOperationQueue alloc]init];默认是异步执行。(接下来也会有提及)
    如果是主队列mainqueue,那么在子线程发送请求成功并获取到服务器的数据响应。就可以回到主线程中解析数据,并刷新UI界面。
  • 如果有刷新UI界面的操作应该放在主线程执行,不能放在子线程。

存在的问题:
1.没有下载进度,会影响用户体验
2.使用异步方法,下载完成执行回调时再把文件写入磁盘,会造成内存峰值的问题。下载文件有多大,NSData就会占用多大的内存

使用代理

问题1的解决办法:使用代理NSURLConnectionDataDelegate
1.在响应方法中获得文件总大小
2.每次接收到数据,计算数据的总长度,和总大小相比,得出百分比

//要下载文件的总长度
@property (nonatomic , assign)long long expectedContentLength;
//当前下载的长度
@property (nonatomic , assign)long long currentLength;

    //1.url
    NSString *urlstr = @"xxxx";
    urlstr = [urlstr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    NSURL *url = [NSURL URLWithString:urlstr];
    
    //2.request
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    
    //3.connection,不做设置的话是在主线程中执行之后的下载
    NSLog(@"start");
    NSURLConnection *conn = [NSURLConnection connectionWithRequest:request delegate:self];
    
    //设置代理工作的操作队列
    [conn setDelegateQueue:[[NSOperationQueue alloc]init]];
    
    //4.启动连接
    [conn start];
//代理
//1.接收到服务器的响应 :状态行和响应头,用来做一些准备工作
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
    NSLog(@"%@",response);//response里面有状态行和响应头
    //记录文件总大小
    self.expectedContentLength = response.expectedContentLength;
    self.currentLength = 0;
}

//2.接收到服务器的数据 :此方法可能会执行多次,因为会接收到多个data
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
    NSLog(@"接收到的数据长度%tu",data.length);
    //计算百分比
    self.currentLength += data.length;
    
    float progress = (float)self.currentLength / self.expectedContentLength;
    NSLog(@"%f",progress);    
}

//3.所有数据加载完成 : 所有数据传输完毕之后被调用,是最后一个的通知
- (void)connectionDidFinishLoading:(NSURLConnection *)connection{
}

//4.下载失败或者错误
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
}

  • 要下载的文件总大小在服务器返回的响应头里面可以拿到
NSDictionary *headerDic = response.allHeaderFields; 
self.fileLength = [[headerDic objectForKey:@"Content-Length"] intValue];

或者

response.expectedContentLength 要下载的文件总大小
response.suggestedFilename 获取下载的文件名
  • +(NSURLConnection*) connectionWithRequest:delegate:
    During the download the connection maintains a strong reference to the delegate. It releases that strong reference when the connection finishes loading, fails, or is canceled. connection对代理方法强引用

问题2的解决办法:
保存文件的思路:
a.拼接完成写入磁盘
b.下载一个写一个
1>NSFileHandle
2>NSOutputStream

** a.拼接完成写入磁盘**
1.生成目标文件路径
2.在didReceiveData里拼接数据
3.拼接完成写入磁盘(在完成下载的方法里)
4.释放内存

//文件保存的路径
@property (nonatomic , strong) NSString *targetFilePath;
//用来每次接收到数据,拼接数据使用
@property (nonatomic , strong) NSMutableData *fileData;
//代理
//1.接收到服务器的响应 :状态行和响应头,用来做一些准备工作
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
    NSLog(@"%@",response);
    
    //放到沙盒
    //NSString *cache = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
    //self.targetFilePath = [cache stringByAppendingPathComponent:response.suggestedFilename];
    //生成目标文件路径    
    self.targetFilePath = [@"/Users/apple/xxxxxx"stringByAppendingPathComponent:response.suggestedFilename];//下载后的文件名字不变
    //删除文件,如果文件存在,就会直接删除,如果文件不存在就什么也不做
    [[NSFileManager defaultManager]removeItemAtPath:self.targetFilePath error:NULL];
}

- (NSMutableData *)fileData{
    if (_fileData == nil) {
        _fileData = [[NSMutableData alloc]init];
    }
    return _fileData;
}

//2.接收到服务器的数据 :次方法可能会执行多次,因为会接收到多个data
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
    NSLog(@"接收到的数据长度%tu",data.length);
    
    //拼接数据
//     a.拼接完成写入磁盘
    [self.fileData appendData:data];
}

//3.所有数据加载完成 : 所有数据传输完毕之后被调用,是最后一个的通知
- (void)connectionDidFinishLoading:(NSURLConnection *)connection{
     //a.拼接完成写入磁盘
    [self.fileData writeToFile:self.targetFilePath atomically:YES];
    //释放内存
    self.fileData = nil;//不释放的话,内存一直被占用,文件多大被占用的就有多大
}
  • 把下载好的文件放到沙盒:
NSString *cache = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
NSString *file = [cache stringByAppendingPathComponent:response.suggestedFilename];```
response.suggestedFilename:建议保存的文件名
- 文件写入磁盘后要释放data:不释放的话,内存一直被占用,文件多大被占用的就有多大

存在的问题:测试结果:也是存在内存峰值

** b.下载一个写一个**
1>NSFileHandle

//2.接收到服务器的数据 :次方法可能会执行多次,因为会接收到多个data

  • (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{

    //拼接数据
    // b.下载一个写一个
    [self writeToFileWithData:data];
    }

  • (void)writeToFileWithData:(NSData )data{
    //文件操作
    /

    NSFileManager:主要功能,创建目录,检查目录是否存在,遍历目录,删除文件...针对文件的操作,类似于Finder
    NSFileHandle:文件“句柄(文件指针)”,如果在开发中,看到Handle这个单词,就意味着是对前面的单词“File”进行操作的对象
    主要功能,就是对同一个文件进行二进制的读写操作的对象。
    */

    //如果文件不存在,fp在实例化的结果是空
    NSFileHandle *fp = [NSFileHandle fileHandleForWritingAtPath:self.targetFilePath];
    //判断文件是否存在
    //如果不存在,直接将数据存入磁盘
    if(fp == nil){
    [data writeToFile:self.targetFilePath atomically:YES];
    }//如果存在,将Data追缴到现有文件
    else{
    //1.将文件指针移到文件的末尾
    [fp seekToEndOfFile];
    //2.写入文件
    [fp writeData:data];
    //3.关闭文件。在c语言开发中,凡是涉及到文件读写,打开和关闭通常是成对出现的
    [fp closeFile];
    }
    }

上面的写法多次打开、关闭文件,下面进行改进:
  • (void)connection:(NSURLConnection )connection didReceiveResponse:(NSURLResponse )response
    {
    NSString
    ceches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
    NSString
    filepath = [ceches stringByAppendingPathComponent:response.suggestedFilename];

    // 创建一个空的文件到沙盒中
    NSFileManager* mgr = [NSFileManager defaultManager];
    [mgr createFileAtPath:filepath contents:nil attributes:nil];
    // 创建一个用来写数据的文件句柄对象
    self.writeHandle = [NSFileHandle fileHandleForWritingAtPath:filepath];
    }

  • (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
    {
    [self.writeHandle seekToEndOfFile];
    [self.writeHandle writeData:data];
    }

  • (void)connectionDidFinishLoading:(NSURLConnection *)connection
    {
    [self.writeHandle closeFile];
    self.writeHandle = nil;
    }

测试结果:彻底解决了内存峰值的问题。是传统的文件操作方式。

2>NSOutputStream输出流
1.创建流
2.打开流
3.将数据追加到流
4.关闭流

//保存文件的输出流
/*

  • (void)open;写入之前打开流
  • (void)close;完成之后关闭流
    */
    @property (nonatomic , strong) NSOutputStream *fileStream;

//代理
//1.接收到服务器的响应 :状态行和响应头,用来做一些准备工作

  • (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
    NSLog(@"%@",response);

    //生成目标文件路径
    self.targetFilePath = [@"/Users/apple/xxxxxx"stringByAppendingPathComponent:response.suggestedFilename];
    //删除文件,如果文件存在,就会直接删除,如果文件不存在没就什么也不做
    [[NSFileManager defaultManager]removeItemAtPath:self.targetFilePath error:NULL];

    self.fileStream = [[NSOutputStream alloc]initToFileAtPath:self.targetFilePath append:YES];
    [self.fileStream open];
    }

//2.接收到服务器的数据 :次方法可能会执行多次,因为会接收到多个data

  • (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{

    //拼接数据
    //将数据追加到文件流中
    [self.fileStream write:data.bytes maxLength:data.length];
    }

//3.所有数据加载完成 : 所有数据传输完毕之后被调用,是最后一个的通知

  • (void)connectionDidFinishLoading:(NSURLConnection *)connection{
    //关闭文件流
    [self.fileStream close];
    }

###断点续传
要实现断点续传要利用HTTP的range请求头。bytes = 500-999表示从500-999的500个字节。续传的Demo:[断点续传](http://www.cnblogs.com/GeekStar/p/4409714.html)
demo里面的例子只适合于应用运行期间续传。比如,一旦应用在下载期间中途退出,再次运行时,下载将会重新开始

// 设置请求头数据
NSString *range = [NSString stringWithFormat:@"bytes=%lld-",self.currentLen];
[request setValue:range forHTTPHeaderField:@"Range"];

多线程断点续传:http://www.cnblogs.com/wendingding/p/3947550.html

###总结
- 小文件下载:可以使用sendAsynchronousRequest:queue:completionHandler 这个方法一次性返回整个下载到的文件,返回的data在内存中,如果下载一个几百兆的东西,这样会造成内存峰值。( [NSData dataWithContentsOfURL:url]这个方法也是一样)
- 大文件下载:使用代理


####NSURLConnection+NSRunLoop
新问题:下载默认下在主线程工作。下载本身是不是异步的(NSURLConnection实例运行在主线程)
 ```[conn setDelegateQueue:[[NSOperationQueue alloc]init]];```
指定了代理的工作队列之后,整个下载仍然会受主线程的干扰,以及更新ui(进度条)不及时。[在storyboard上添加了一个进度条以及一个uitextview,下载的时候,进度条会卡顿,滑动textview下载会暂停,停止滑动后又继续下载]
 
``` NSURLConnection *conn = [NSURLConnection connectionWithRequest:request delegate:self];```在这里,delegate参数在api里面的说明如下:
 The delegate object for the connection. The connection calls methods on this delegate as the load progresses. Delegate methods are called on the same thread that called this method. For the connection to work correctly, the calling thread’s run loop must be operating in the default run loop mode.代理方法被调用connectionWithRequest:delegate:这个方法的同一个线程调用,为了保证连接的工作正常,调用线程的runloop必须运行在默认的运行循环模式下。

[一个异步网络请求的坑:关于NSURLConnection和NSRunLoopCommonModes](http://www.hrchen.com/2013/06/nsurlconnection-with-nsrunloopcommonmodes/)这篇博文有提到这个问题:
如果是直接调用initWithRequest:delegate:startImmediately:(第三个参数用YES)或者方法initWithRequest:delegate:时(调用完connection就直接运行了),NSURLConnection会默认运行在**NSDefaultRunLoopMode**模式下,即使再使用scheduleInRunLoop:forMode:设置运行模式也没有用。如果NSURLConnection是运行在NSDefaultRunLoopMode,而当前线程是**主线程**(NSURLConnection实例运行在主线程,主线程有一个运行的runloop实例来支持NSURLConnection的异步执行),并且UI上有类似滚动这样的操作,那么主线程的Run Loop会运行在**UITrackingRunLoopMode**下,就无法响应NSURLConnnection的回调。此时需要首先使用initWithRequest:delegate:startImmediately:(第三个参数为NO)生成NSURLConnection,再重新设置NSURLConnection的运行模式为**NSRunLoopCommonModes**,那么UI操作和回调的执行都将是非阻塞的,因为NSRunLoopCommonModes是一组run loop mode的集合,默认情况下包含了NSDefaultRunLoopMode和UITrackingRunLoopMode。
  • (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode在api中的说明
    将connection实例回调加入到一个runloop,NSURLConnectionDelegate回调会在这个runloop中响应
    Determines the run loop and mode that the connection uses to call methods on its delegate.
    By default, a connection is scheduled on the current thread in the default mode when it is created. If you create a connection with the initWithRequest:delegate:startImmediately: method and provide NO for the startImmediately parameter, you can schedule the connection on a different run loop or mode before starting it with the start method. You can schedule a connection on multiple run loops and modes, or on the same run loop in multiple modes.
    You cannot reschedule a connection after it has started.It is an error to schedule delegate method calls with both this method and the setDelegateQueue: method.
    方法参数:
    aRunLoop:The NSRunLoop instance to use when calling delegate methods。
    mode:The mode in which to call delegate methods.
    这个方法不能和setDelegateQueue方法一起使用

总结一下个人理解,默认情况下,代理方法被调用connectionWithRequest:delegate:这个方法的同一个线程调用,NSURLConnection默认运行在NSDefaultRunLoopMode模式下;但是使用scheduleInRunLoop: forMode:可以设置代理方法运行在哪个runloop(相当于是设置运行线程?)和mode下

//运行在主线程下
NSMutableURLRequest* request = [[NSMutableURLRequest alloc]
initWithURL:self.URL
cachePolicy:NSURLCacheStorageNotAllowed
timeoutInterval:self.timeoutInterval];
self.connection =[[NSURLConnection alloc] initWithRequest:request
delegate:self
startImmediately:NO];
//设置回调代理方法运行在那个runloop和mode,这里设置在当前runloop也就是主线程里面执行代理方法
[self.connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[self.connection start];


**1.为了不影响ui线程,把下载工作放到子线程里面**

dispatch_async(dispatch_get_global_queue(0, 0), ^{//这样写,下载不执行了。[conn start];之后子线程被回收释放内存空间
//1.url
NSString *urlstr = @"xxxx";
urlstr = [urlstr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL *url = [NSURL URLWithString:urlstr];

    //2.request
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    
    //3.connection,不做设置的话是在主线程中执行之后的下载 
    NSLog(@"start,%@",[NSThread currentThread]);
    NSURLConnection *conn = [NSURLConnection connectionWithRequest:request delegate:self];
    
    //设置代理工作的操作队列
    [conn setDelegateQueue:[[NSOperationQueue alloc]init]];
    
    //4.启动连接
    [conn start];

});

把网络操作方法放到子线程执行以后,回调代理方法无法执行,无法下载。
原因:主线程runloop会自动启动,子线程runloop默认不启动。将网络操作放在异步执行,异步的runloop不启动,没有办法监听到网络事件。[conn start];之后子线程被回收释放内存空间。

[一个异步网络请求的坑:关于NSURLConnection和NSRunLoopCommonModes](http://www.hrchen.com/2013/06/nsurlconnection-with-nsrunloopcommonmodes/)这篇博文有提到这个问题:如果是用GCD在其他线程中启动NSURLConnection:不会得到NSURLConnection回调,而从主线程中启动NSURLConnection可以得到回调,这是由于在GCD全局队列中执行时没有运行Run Loop,那么NSURLConnection也就无法触发回调了。
解决的办法如下:

**2.启动子线程runloop**
a.[[NSRunLoop currentRunLoop] run];使用这种方法使用,runloop永远释放不掉
b.开启一个循环,判断下载是否完成。这种方法对系统消耗非常大

@property (nonatomic , assign , getter = isFinished) BOOL finished;

//主线程runloop会自动启动,子线程runloop默认不启动
//将网络操作放在异步执行,异步的runloop不启动,没有办法监听到网络事件
dispatch_async(dispatch_get_global_queue(0, 0), ^{//这样写,下载不执行了。[conn start];之后子线程被回收释放内存空间
    ......
    
    //4.启动连接
    [conn start];
    
    self.finished = NO;
    //5.启动运行循环

// a. [[NSRunLoop currentRunLoop] run];//这样写永远释放不掉
// b.
while (!self.isFinished) {
//启动一个死循环,每次监听0.1秒.
//对系统消耗非常大
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
}
});

  • (void)connectionDidFinishLoading:(NSURLConnection *)connection{
    ......
    self.finished = YES;
    }

c.方法b的改进,例子里面使用到CFRunLoop

@property (nonatomic , assign) CFRunLoopRef rl;

//主线程runloop会自动启动,子线程runloop默认不启动
//将网络操作放在异步执行,异步的runloop不启动,没有办法监听到网络事件
dispatch_async(dispatch_get_global_queue(0, 0), ^{//这样写,下载不执行了。[conn start];之后子线程被回收释放内存空间
    
    //1.url
    NSString *urlstr = @"xxxx";
    urlstr = [urlstr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    NSURL *url = [NSURL URLWithString:urlstr];
    
    //2.request
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    
    //3.connection,不做设置的话是在主线程中执行之后的下载
    //开始时的线程是由dispatch_async 创建的
    NSLog(@"start,%@",[NSThread currentThread]);
    NSURLConnection *conn = [NSURLConnection connectionWithRequest:request delegate:self];
    
    //设置代理工作的操作队列
    [conn setDelegateQueue:[[NSOperationQueue alloc]init]];
    //指定调度代理工作的操作队列。操作队列的特点:添加任务,立即异步执行,具体的线程程序员不能决定
    
    //4.启动连接
    [conn start];
    
    //5。启动运行循环
    //CF框架

// CFRunLoopStop(CFRunLoopRef rl);停止指定的runloop
// CFRunLoopGetCurrent();当前线程的runloop
// CFRunLoopRun();直接运行当前线程的runloop
//1.拿到当前的runloop
self.rl = CFRunLoopGetCurrent();
//2.启动运行循环
CFRunLoopRun();
});

  • (void)connectionDidFinishLoading:(NSURLConnection *)connection{
    //结束时代理工作的线程,是由指定的NSOperationQueue创建的,和创建下载操作的线程是不一样的。
    //关闭runloop,要关闭指定线程上的runloop。在这里拿到创建下载那个线程的runloop
    NSLog(@"finish,%@",[NSThread currentThread]);
    //关闭文件流
    ......
    CFRunLoopStop(self.rl);
    }
![Runloop的启动和关闭示意图.png](http://upload-images.jianshu.io/upload_images/1727123-40091d2fffa8ac38.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
- 开始时的下载线程是由dispatch_async 创建的,```NSLog(@"start,%@",[NSThread currentThread]);```
 结束时**代理方法**工作的线程,是由指定的NSOperationQueue创建的:```[conn setDelegateQueue:[[NSOperationQueue alloc]init]];```,和创建下载操作的线程是不一样的。```NSLog(@"finish,%@",[NSThread currentThread]);```这里打印的两个线程结果是不一样的。
- ```[conn setDelegateQueue:[[NSOperationQueue alloc]init]];```指定调度**代理方法**工作的操作队列。操作队列的特点:添加任务,立即异步执行,具体的线程程序员不能决定.
- 启动runloop:1.拿到当前的runloop ```self.rl = CFRunLoopGetCurrent();```2.启动runloop``` CFRunLoopRun();```
 关闭runloop:要关闭指定线程上的runloop。在这里拿到创建下载那个线程的runloop ```CFRunLoopStop(self.rl);```

相关文章:https://blog.cnbluebox.com/blog/2014/07/01/cocoashen-ru-xue-xi-nsoperationqueuehe-nsoperationyuan-li-he-shi-yong/

你可能感兴趣的:(NSURLConnection文件下载和NSRunLoopCommonModes)