在了解GCD之前,我们首先要知道几个概念。关于队列和同/异步函数。为了让读者更简单直观的理解这些概念,我尽可能用最简单的话进行解释。
关于队列
主队列:专门在主线程上调度任务。
串行队列:顾名思义,也就是任务一个接着一个的执行(按顺序执行)。
并发队列:顾名思义,任务可以同时执行。
全局队列:本质上还是并发队列。
关于同/异步函数
同步函数:不会开启新线程。在主线程马上执行任务。
异步函数:会开启新线程。不会马上执行任务。
队列和同/异步函数联合使用
主队列+同步函数
上面说过,开启同步任务不会开启新线程,而是在主线程马上执行任务。但是主线程有正在执行的任务,只有主线程的任务执行完毕,才能执行主队列中新加的任务。而主队列也在等待主线程的任务执行完毕,然后执行自己的任务。
主队列+异步函数
上面说过,异步函数不会马上执行任务,并且会开启新线程。但是因为是在主队列中使用异步函数(因为主队列是在主线程上调度任务),所以既不会马上执行任务,也不会开启新线程。而是等主线程的任务全部执行完毕后,等主线程有空了再去执行异步任务。看看代码效果。
-(void)test {
NSLog(@"touchesBegan");
// 主队列+异步函数
// 等主线程任务执行完毕之后,再执行 test2
dispatch_async(dispatch_get_main_queue(), ^{
[self test2];
});
[self test3];
NSLog(@"touchesEnd");
}
- (void)test2 {
NSLog(@"test2-----------%@",[NSThread currentThread]);
}
- (void)test3 {
NSLog(@"test3-----------%@",[NSThread currentThread]);
}
控制台输出:
number = 1, name = main,证明任务是在当前线程(主线程)执行的。
使用场景:
1.回到主线程更新UI
2.调整任务执行顺序
同步函数+其他队列
串行队列:会在当前线程同步执行。
并发队列:会在当前线程同步执行,这种使用方式没有意义,不推荐。
异步函数+其他队列
串行队列:因为串行队列是一个执行完后再执行下一个,所以只会多开一条新线程调度任务。
并发队列:因为并发队列是多个任务同时执行,所以会开多条新线程调度任务。
GCD
线程间通信
例:开启一条子线程进行下载图片的耗时操作,图片下载完后回到主线程更新UI。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 1. 开启子线程下载图片
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 下载图片!
UIImage *image = [self downloadWebImage];
// 回到主线程显示图片!
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"显示图片:%@",[NSThread currentThread]);
// 显示图片!
// 如果设置按钮的图片,必须改变按钮的类型为 custom!
//[self.button setImage:image forState:UIControlStateNormal];
// 设置按钮的背景图片:
[self.button setBackgroundImage:image forState:UIControlStateNormal];
});
});
}
// 下载网络图片
// 返回值: 下载好的图片!
-(UIImage *)downloadWebImage{
NSLog(@"downloadWebImage:%@",[NSThread currentThread]);
NSString *urlString = @"http://d.hiphotos.baidu.com/image/pic/item/64380cd7912397dd2a7d71d15d82b2b7d1a287db.jpg";
NSURL *url = [NSURL URLWithString:urlString];
// 这是一个耗时方法!这一个方法内部封装了很多代码(关于网络的代码!)!
NSData *data = [NSData dataWithContentsOfURL:url];
// 下载好的图片
UIImage *image = [UIImage imageWithData:data];
return image;
}
调度组
关键字: dispatch_group_t
作用:管理队列的!
(1)参数说明
参数1:标识队列组!
参数2:管理的队列!
参数3:添加到队列中的任务!
dispatch_group_async(<#dispatch_group_t group#>, <#dispatch_queue_t queue#>, <#^(void)block#>)
(2)关键函数
等待队列组中的任务都执行完毕之后,就会发出通知,调用下面的方法!
队列组: 等待哪一个队列组中的任务执行完毕!
队列: 决定后续的任务在哪条线程执行
任务: 后续的任务!
dispatch_group_notify(<#dispatch_group_t group#>, <#dispatch_queue_t queue#>, <#^(void)block#>)
需求:下载两张图片,下载完成后,合并两张图片并显示
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// 创建一个队列组!
dispatch_group_t group = dispatch_group_create();
//变量声明
__block UIImage *image1 ,*image2;
// 下载第一张图片
dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
image1 = [self downloadImageWithUrlString:@"http://g.hiphotos.baidu.com/image/pic/item/95eef01f3a292df54e0e7e08be315c6035a873da.jpg"];
});
// 下载第二张图片
dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
image2 = [self downloadImageWithUrlString:@"http://e.hiphotos.baidu.com/image/pic/item/cc11728b4710b912d4bb69ffc1fdfc03924522bc.jpg"];
});
// 合并图片并且显示
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 合并图片
UIImage *image = [self bingImageWithImage1:image1 Image2:image2];
// 显示合并之后的图片!
self.imageView.image = image;
});
}
// 合并图片
-(UIImage *)bingImageWithImage1:(UIImage *)image1 Image2:(UIImage *)image2{
// 1.开启图形上下文
UIGraphicsBeginImageContext(self.imageView.bounds.size);
//2.绘制第一张图片
[image1 drawInRect:self.imageView.bounds];
// 3.绘制第二张图片
[image2 drawInRect:CGRectMake(0, self.imageView.bounds.size.height - 80, self.imageView.bounds.size.width, 80)];
// 4.获取绘制好的图片
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
// 5.关闭图形上下文
UIGraphicsEndImageContext();
return image;
}
// 下载图片
-(UIImage *)downloadImageWithUrlString:(NSString *)urlString{
NSURL *url = [NSURL URLWithString:urlString];
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:data];
return image;
}
一次性代码
使用场景:单例。它确保代码只被执行一次,不管有多少个线程。GCD的dispatch_once可以确保代码只被执行一次。
@interface Person : NSObject
+ (instancetype)sharedPerson;
@end
@implementation Person
+ (instancetype)sharedPerson{
static id instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
@end
阻塞式函数
使用场景:任务1和任务2同时执行,然后执行任务3,然后任务4和任务5同时执行。
需要注意:阻塞式函数只有自己创建的并发队列才有用,对全局并发队列无效。
关键字: dispatch_barrier_async
- (void)test {
// 需求:任务 1~5 ,执行顺序:
// 第三个任务必须等待前两个任务执行完毕之后,再执行!
// 后两个任务必须等待第三个任务完成之后再执行!
// 任务1和2 同时执行, 任务4和5同时执行!
// 阻塞式函数! -- 主要是阻塞并发队列的!(只有自己创建的并发队列才有效,全局并发队列不可以使用阻塞式函数).
// 1.队列:需要阻塞哪一个队列
// 2.任务:阻塞队列的任务! 任务3/阻塞式任务!
// dispatch_barrier_async(<#dispatch_queue_t queue#>, <#^(void)block#>)
// 创建并发队列,按顺序往并发队列中添加任务!
// 创建一个并发队列
dispatch_queue_t queue = dispatch_queue_create(NULL, DISPATCH_QUEUE_CONCURRENT);
// 按顺序往并发队列中添加任务
dispatch_async(queue, ^{
//模拟耗时操作
[NSThread sleepForTimeInterval:5];
NSLog(@"任务1:%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
//模拟耗时操作
[NSThread sleepForTimeInterval:3];
NSLog(@"任务2:%@",[NSThread currentThread]);
});
// 任务3是一个阻塞式任务,利用阻塞式函数添加
// 同步和异步都是相对于当前线程来说的!
dispatch_barrier_sync(queue, ^{
//模拟耗时操作
[NSThread sleepForTimeInterval:5];
NSLog(@"任务3:%@",[NSThread currentThread]);
[NSThread sleepForTimeInterval:5];
});
dispatch_async(queue, ^{
NSLog(@"任务4:%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"任务5:%@",[NSThread currentThread]);
});
}
延时执行
这里不止有GCD的延时执行方法。在了解这两个延时执行方法之前我们还是要了解一些和Runloop有关的概念性知识,因为这很有必要。当然只是做一些简单的介绍,深入的介绍会在以后陆续讲解。
1、主线程和子线程的区别
主线程的运行循环默认是开启的,子线程的运行循环默认是关闭的!
2、Runloop
运行循环驱动(执行)事件源(UI操作/点击/滚动/定时器/特殊的事件)。
运行循环是一个死循环(do...while...循环)!平时没有事件驱动的时候,运行循环就处于睡眠状态!当有一些事件源发生的时候,就会唤醒运行循环来执行事件!事件执行完毕之后,运行循环接着进入睡眠状态!
运行循环的开启需要事件源的驱动!一个线程中,如果没有事件源,运行循环无法开启!
3、两种延迟执行的方式
(1) GCD
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(<#delayInSeconds#> * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
<#code to be executed after a specified delay#>
});
(2) other
self performSelector:<#(nonnull SEL)#> withObject:<#(nullable id)#> afterDelay:<#(NSTimeInterval)#>
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"touchesBegan");
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self performSelector:@selector(test) withObject:nil afterDelay:3.0];
NSLog(@"能够到这里吗?");
});
NSLog(@"touchesEnd");
}
- (void)test {
NSLog(@"test");
}
输出结果:
通过控制台输出可以看到test方法的Log并没有打印。
为什么呢?
方式(2)是一个事件源,一个特殊的事件源,这句代码执行完后,会自动关闭运行循环。
需要延迟执行的test方法并没有执行,因为这句代码执行完后就关闭了运行循环。
所以为了延迟执行test方法,需要在执行完这句代码后开启运行循环,代码 [[NSRunLoop currentRunLoop] run];
[[NSRunLoop currentRunLoop] run]最好写成 CFRunLoopRun();这样写方便我们关闭它。
因为NSRunLoop并没有为我们提供关闭它的方法,而 CFRunLoopRun()给我们提供了关闭运行循环的方法,如下
CFRunLoopGetCurrent():获得当前线程的运行循环!
CFRunLoopStop(CFRunLoopGetCurrent()); 关闭运行循环!
所以还要注意,如果要在一个子线程中开启一个定时器,把当前定时器添加到运行循环后,一定要开启运行循环。(因为子线程的运行循环默认是关闭的)。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"touchesBegan");
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//创建一个定时器
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
//把定时器加入到当前的运行循环
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
//开启运行循环
CFRunLoopRun();
NSLog(@"come here! %@",[NSThread currentThread]);
});
NSLog(@"touchesEnd");
}
- (void)test {
static int i = 0;
NSLog(@" i = %d",i);
if (i == 6) {
CFRunLoopStop(CFRunLoopGetCurrent());
NSLog(@"关闭运行循环!");
}
}
控制台输出:
NSOperation & NSOperationQueue
NSOperation是对GCD的封装,面向操作编程。
NSOperation有两种方式创建操作对象
(1)
NSInvocationOperation *opInvo = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(test) object:nil];
(2)
NSBlockOperation *opBlock = [NSBlockOperation blockOperationWithBlock:^{
//xxxx
}];
NSOperation的执行
第一种情况:
将操作添加到队列中
[queue addOperation:op];
使用这种方式向队列中添加操作,所有操作都会在子线程中执行。
所以,建议如果NSBlockOperation需要在主线程执行,就不要追加操作。
第二种情况:
调用start方法
[op start];
NSBlockOperation 如果追加了任务! 直接调用 start 方法或者将操作添加到主队列, 这个时候,操作中的任务会在不同的线程执行(主线程 + 子线程)。
两种队列
主队列:在主线程中完成操作
非主队列:在子线程中完成操作
(1)创建主队列
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
// 添加操作到队列中!
// 将操作添加到队列中之后,就会自动执行 NSOperation 对象的 main 方法!
[queue1 addOperation:op];
(2)创建非主队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 添加操作到队列中!
// 将操作添加到队列中之后,就会自动执行 NSOperation 对象的 main 方法!
[queue addOperation:op];
(3)往队列中添加操作的简便写法
// 缺点: 没有一个操作对象.不能对操作做后续的管理!
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
NSLog(@"任务:%@",[NSThread currentThread]);
}];
NSBlockOperation追加任务及需要注意的点
- 创建一个操作
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"任务1 %@",[NSThread currentThread]);
}];
2.NSBlockOperation 可以追加任务!
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"op1 - %@",[NSThread currentThread]);
}];
[op addExecutionBlock:^{
NSLog(@"任务2 %@",[NSThread currentThread]);
}];
[op addExecutionBlock:^{
NSLog(@"任务3 %@",[NSThread currentThread]);
}];
[op addExecutionBlock:^{
NSLog(@"任务4 %@",[NSThread currentThread]);
}];
[op start];
控制台输出:
也就是说追加的任务全部是在子线程中执行的!
NSOperation的高级使用方法
操作依赖
比如有如下需求:
有5个操作1-5,操作1-3在子线程中执行,操作4-5在主线程中执行,并且操作的执行顺序是1,2,3,4,5。这个时候就需要使用操作依赖。让后一个操作依赖前一个操作,并且操作依赖可以夸队列(线程)。
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"任务1 %@",[NSThread currentThread]);
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"任务2 %@",[NSThread currentThread]);
}];
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"任务3 %@",[NSThread currentThread]);
}];
NSBlockOperation *op4 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"任务4 %@",[NSThread currentThread]);
}];
NSBlockOperation *op5 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"任务5 %@",[NSThread currentThread]);
}];
// 创建非主队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 保证操作按顺序执行 --- 添加操作依赖!
// 能够保证先执行 op2 ,再执行 op1
// 操作依赖内部使用了:线程同步技术!
// 添加操作依赖注意:
// 注意1: 不要添加循环依赖!
// 注意2: 一定要先添加操作依赖,然后再把操作添加到操作队列中!
// 注意3: 对不不同队列中的操作,添加操作依赖依然有效!
[op2 addDependency:op1];
[op3 addDependency:op2];
[op4 addDependency:op3];
[op5 addDependency:op4];
[queue addOperation:op1];
[queue addOperation:op2];
[queue addOperation:op3];
NSOperationQueue *queue2 = [NSOperationQueue mainQueue];
[queue2 addOperation:op4];
[queue2 addOperation:op5];
控制台的输出一定是按照顺序输出的!
队列操作
操作队列可以管理操作,可以 暂停/恢复/取消操作, 实际开发中,为了在任何时候都能够使用这个队列,做成全局的!
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// 暂停队列中的操作
[queue setSuspended:YES];
// 恢复队列中的操作
[queue setSuspended:NO];
// 取消队列中的所有操作
// 取消操作:对于已经开始的操作是无法取消的!
// 系统接收到内存警告的时候!
[queue cancelAllOperations];
// 取消单个操作!
[op cancel];
队列间线程通信
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperationWithBlock:^{
// 1.异步下载图片
NSURL *url = [NSURL URLWithString:@"http://d.hiphotos.baidu.com/image/pic/item/37d3d539b6003af3290eaf5d362ac65c1038b652.jpg"];
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:data];
// 2.回到主线程,显示图片
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.imageView.image = image;
}];
}];
如何选择使用GCD还是NSOperation ?
首先,NSOperation是对GCD的封装,在使用上当然是GCD更具有效率。
NSOperation是面向操作编程,GCD是面向任务编程。
使用NSOperation我们可以将某个操作暂停,恢复或取消操作。可以满足许多使用场景,比如一个tableView上下滚动的时候,如果开始滚动就暂停下载操作,如果停止滚动就恢复下载操作。如果系统提示内存警告还可以取消操作。
但是GCD就不具备NSOperation的上述功能,GCD适用一些简单的需求,比如简单的多线程操作等等。
所以根据具体需求选择相应的实现技术。