利用运行循环解决NSURLConnection多线程下载的问题(只需要了解)

NSURLConnection问题:

1. 做复杂的网络操作,需要使用代理来实现。比如下载大文件

2. 默认下载任务在主线程工作。

3. 默认这个任务的代理也是在主线程

4. 如果添加到子线程去执行,需要主动启动运行循环

5. 只提供开始和取消。不支持暂停。

代码如下:

//
//  ViewController.m
//  11-NSURLConnecntion下载
//
//  Created by apple on 15/1/22.
//  Copyright (c) 2015年 apple. All rights reserved.
/**
 
 NSURLConnection存在的问题,iOS2.0就有了。 专门用来负责网络数据的传输,已经有10多年的历史
 
 特点:
 - 处理简单的网络操作,非常简单
 - 但是处理复杂的网络操作,就非常繁琐
 ASI&AFN
 
 iOS 5.0以前,网络的下载是一个黑暗的时代
 *** iOS5.0以前 通过代理的方式来处理网络数据
 
 存在的问题:
 1.下载的过程中,没有”进度的跟进“ -- 用户的体验不好
 2.存在内存的峰值
 
 
 解决进度跟进的问题
 解决办法:通过代理的方式来处理网络数据
 
 代理还是出现峰值, 是因为全部接受完了,再去写入
 解决办法,接收到一点,写一点
 //    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(click) userInfo:nil repeats:YES];
 ////    NSDefaultRunLoopMode : 默认的运行循环模式。 处理的优先级比NSRunLoopCommonModes低
 ////    NSRunLoopCommonModes : 通用的模式,在用户拖动屏幕的时候同样执行
 //    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
 
 NSURLConnection的代理,默认是在主线程运行的
 需要把他的代理 移到子线程执行
 
 下载这个任务 本身还在主线程。 所有导致进度条的更新非常卡
 主线程现在在做两件事,1. 下载 2.更新进度条
 
 NSURLConnection问题:
 1. 做复杂的网络操作,需要使用代理来实现。 比如下载大文件
 2. 默认下载任务在主线程工作。
 3. 默认这个任务的代理也是在主线程
 4. 如果添加到子线程去执行,需要主动启动运行循环
 5. 只提供开始和取消。 不支持暂停。
 
 */

#import "ViewController.h"

// NSURLConnectionDownloadDelegate这个代理仅适用”杂志的下载“
@interface ViewController () 

@property (weak, nonatomic) IBOutlet UIProgressView *progressView;

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

// 当前下载的长度
@property(nonatomic,assign)long long currentLenght;

/**保存文件的目标路径*/
@property(nonatomic,copy)NSString *targetPath;

/**下载完成的标记*/
@property(nonatomic,assign)BOOL finished;

@end

@implementation ViewController

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    
    // 把这段代码 移动子线程
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        
        
        
        // 下载 GET
        // 1. url
        NSString *urlStr = @"http://127.0.0.1/01-课程概述.mp4";
        // 百分号转义
        urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
        
        NSURL *url = [NSURL URLWithString:urlStr];
        
        // 2. 请求
        NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:1 timeoutInterval:2.0f];
        
        
        // 3. 创建连接
        NSURLConnection *connect = [NSURLConnection connectionWithRequest:request delegate:self];
        
        // 设置代理工作的队列(把代理的工作放到子线程)
        [connect setDelegateQueue:[[NSOperationQueue alloc] init]];
        
        // 4. 启动网络连接
        [connect start];
        
        // 子线程开启运行循环, 这个死循环 肯定要想办法关闭
        
        do{
            //            [[NSRunLoop currentRunLoop] run];
            // 让运行循环每隔0.1秒运行一下
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
        }while(!self.finished);
        
    });
}

#pragma mark - 实现方法
// 1. 接收到服务器的响应 - 做好准备
/**
 
 NSURLResponse:响应
 URL:请求资源的路径
 MIMEType(Content-Type):  返回的”二进制数据“的类型
 expectedContentLength:预期的文件的长度,对于下载来说,就是文件的大小
 textEncodingName:文本的编码名称
 ***
 UTF-8 - 几乎涵盖了全世界200多个国家的语言文字
 GB2312 - 国内的一些老的网站可能还在使用这个编码 包含了6700+汉字 85000+
 suggestedFilename: 服务器建议 的文件名
 */
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
    
    // 文件的大小
    NSLog(@"文件的长度%lld", response.expectedContentLength);
    
    // 记录文件的总长度
    self.expectedContentLength = response.expectedContentLength;
    
    // 将下载的长度清零
    self.currentLenght = 0;
    
    // 设置文件的目标路径
    self.targetPath = [@"/Users/apple/Desktop/" stringByAppendingPathComponent:response.suggestedFilename];
    
    
    //    if (data.length == self.expectedContentLength || data.length < self.expectedContentLength) {
    
    // 简单粗暴, 准备接受文件数据之前,判断如果有这个文件,直接删掉
    [[NSFileManager defaultManager] removeItemAtPath:self.targetPath error:NULL];
    
    
    //    }
    
}

// 2. 接受到服务器返回的数据 - 会调用多次
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    //    NSLog(@"接收到数据, 可以拼接所有的数据%tu", data.length);
    
    // 记录当前已经下载的文件的长度
    self.currentLenght += data.length;
    // 计算进度()
    float progress = (float)self.currentLenght/self.expectedContentLength;
    
    NSLog(@"进度%f,  ---%@", progress, [NSThread currentThread]);
    
    // 更新UI进度条
    dispatch_async(dispatch_get_main_queue(), ^{
        
        self.progressView.progress = progress;
    });
    
    // 建立文件句柄, 准备写入到targetPath
    NSFileHandle *fp = [NSFileHandle fileHandleForWritingAtPath:self.targetPath];
    
    // 如果文件不存在,句柄就是nil。 这时候没法操作文件的
    if (fp == nil) {
        [data writeToFile:self.targetPath atomically:YES];
    }else {
        
        // 将句柄移到当前文件的末尾
        [fp seekToEndOfFile];
        
        // 将数据写入(以句柄为参照来 开始写入的)
        [fp writeData:data];
        
        // 在C语言中,所有的文件的操作完成以后,都需要关闭文件,这里也需要关闭。
        // 为了保证文件的安全
        [fp closeFile];
    }
}

// 3. 所有的数据传输完毕
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    NSLog(@"所有的数据传输完毕,写成文件");
    
    self.finished = YES;
}

// 4. 下载过程中,出现错误
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
    NSLog(@"注意:网络请求的时候,一定要注意错误处理");
}
@end

需要掌握和注意的知识点如下:

注意:如果创建句柄对象的路径指向的文件不存在,那么句柄对象就是nil

下载操作不能在主线程运行。否则会阻塞主线程。例如当正在下载时进行UI更新时就会使下载终止。

(因为更新UI的优先级要比网络和NSTimer计时器的默认优先级高)。

NSDefaultRunLoopMode:默认的运行循环模式。处理的优先级要比NSRunLoopCommonModes低。

NSRunLoopComonModes:通用的一种模式,能够保证用户在拖动屏幕的时候同样执行。

当我们使用NSTimer计时器重复执行一项操作时,为了在操作执行的过程中不会因为拖拽屏幕而终止,需要将NSTimer计时器添加到当前循环并指定NSRunLoopCommonModes模式,设置此模式后。当用户在拖拽屏幕的过程中处理器就会不断循环轮流执行组件操作和计时器操作。也就是说处理器也分出一些时间片来进行处理计时器操作。

用法如下:NSTimer *timer = [NSTimer timeWithTimeInterval:1.0 target:self selector: @selector(click) userInfo:nil repeats: YES];

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];


网络连接NSURLConnectionNSTimer类似也存在运行循环问题也是默认的NSDefaultRunLoopMode模式。所以当执行优先级比较高的操作比如更新UI、拖动屏幕时下载就会停滞,当UI操作完毕后才继续下载。此问问题的解决方法如下:

第一步:通过设置代理工作的队列把代理的工作放到子线程。(NSURLConnection的代理默认是在主线程运行的,所以把它的代理移到子线程执行)例如:NSURLConnection *connect = [NSURLConnection connectionWithRequest:request delegate: self]; 在没有设置的情况下,是在代理对象delegate的主线程中执行代理协议中的方法的。具体设置方法如下:

[connect setDelegateQueue:[[NSOperationQueue alloc] init];

第二步:虽然代理执行的操作内容放到了子线程,但下载任务仍在主线程。下载任务和更新进度条的UI操作都在主线程,才会抢夺资源导致进度条非常卡。所以需要把下载任务的操作放到子线程。

思维误区:下载任务与下载的代理是不同的。当下载每执行到指定的步骤才会调用代理方法,真正的下载任务默认也是在当前控制器主线程的。

第三步:但是当我们直接把下载任务触发事件放到GCD的异步操作内,却无法执行。原因主要是因为:每开启一个线程都默认开启一个运行循环,但主线程的运行循环是默认开启的所以主线程能执行触发事件的操作。而别的新开启的线程是默认不开启的,所以子线程是无法处理事件操作的。所以要进行开启后才能执行事件操作。具体方法如下所示:

[[NSRunLoop currentRunLoop] run];

注意:线程的运行循环是专门监听事件处理的,子线程的运行循环默认是不开启的,所以无法处理事件。所以子线程必须开启运行循环后才能处理事件。

第四步:由于我们开启了子线程的死循环,如果不关闭开启后的运行循环,那么这个子线程是无法释放的。所以我们要加一个判断标记如下所示:

do{

// [[NSRunLoop currentRunLoop] run];

// 让运行循环每隔0.1秒运行一下

[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];

}while(!self.finished);


运行循环理解如下:

Runloop是事件接收和分发机制的一个实现。

Runloop提供了一种异步执行代码的机制,不能并行执行任务。

在主队列中,Main RunLoop直接配合任务的执行,负责处理UI事件、定时器以及其他内核相关事件。

RunLoop的主要目的:

保证程序执行的线程不会被系统终止。

什么时候使用Runloop ?

当需要和该线程进行交互的时候才会使用Runloop.

每一个线程都有其对应的RunLoop,但是默认非主线程的RunLoop是没有运行的,需要为RunLoop添加至少一个事件源,然后去run它。

一般情况下我们是没有必要去启用线程的RunLoop的,除非你在一个单独的线程中需要长久的检测某个事件。

主线程默认有Runloop。当自己启动一个线程,如果只是用于处理单一的事件,则该线程在执行完之后就退出了。所以当我们需要让该线程监听某项事务时,就得让线程一直不退出,runloop就是这么一个循环,没有事件的时候,一直卡着,有事件来临了,执行其对应的函数。

Runloop,正如其名所示,是线程进入和被线程用来响应事件以及调用事件处理函数的地方。需要在代码中使用控制语句实现run loop的循环,也就是说,需要代码提供while 或者 for循环来驱动run loop。

在这个循环中,使用一个Runloop对象[NSRunloop currentRunloop]执行接收消息,调用对应的处理函数。

Runloop接收两种源事件:input sources和timer sources。

input sources传递异步事件,通常是来自其他线程和不同的程序中的消息;

timer sources(定时器)传递同步事件(重复执行或者在特定时间上触发)。

除了处理input sources,Runloop 也会产生一些关于本身行为的notificaiton。注册成为Runloop的observer,可以接收到这些notification,做一些额外的处理。(使用CoreFoundation来成为runloop的observer)。

Runloop工作的特点:

1> 当有事件发生时,Runloop会根据具体的事件类型通知应用程序作出响应;

2> 当没有事件发生时,Runloop会进入休眠状态,从而达到省电的目的;

3> 当事件再次发生时,Runloop会被重新唤醒,处理事件。

提示:一般在开发中很少会主动创建Runloop,而通常会把事件添加到Runloop中。

你可能感兴趣的:(利用运行循环解决NSURLConnection多线程下载的问题(只需要了解))