iOS多线程的方法有3种:
- NSThread
- NSOperation
- GCD(Grand Central Dispatch)
其中,由苹果所倡导的为多核的并行运算提出的解决方案:GCD能够访问线程池,并且可在应用的整个生命的周期里面使用,一般来说,GCD会尽量维护一些适合机器体系结构的线程,在有工作需求的时候,自动利用更多的处理器核心,以此来充分使用更强大的机器系统性能。在以前,iOS设备为单核处理器的,线程池的用处并不大,但是现在的移动设备,包括iOS设备,愈发地朝多核的方向迈进,因此GCD中的线程池,能够在此类设备中,能够使得强大的硬件系统性能上得到更加完善的利用。
GCD,无疑是最便捷的,基于C
语言的所设计的。在使用GCD的过程中
,最方便的,莫过于不需要编写基础线程代码,其生命周期也不需要手动管理;创建需要的任务,然后添加到已创建好的queue队列,GCD便
会负责创建线程和调度任务,由系统直接提供线程管理。
这样一种多线程的方式,我们也会在实际项目中经常看到:app中,由于数据的执行与交换所消耗的时间长,导致需要反馈给用户UI界面往往出现延迟的现象。这样我们可以通过多线程的方法,让需要调用的方法在后台执行、在主线程上进行UI界面的切换,这样不仅是用户体验更加友好美观,也使得程序设计井然有序。
本文主要粗略介绍GCD的一般使用,以及GCD中dispatch_前缀方法调用的作用和使用范围。
UI界面如下图,通过创建4个按钮事件,分析4种不同的函数所执行的程序块运行方式:
【本次开发环境: Xcode:7.2 iOS Simulator:iphone6 By:啊左】
一、GCD的使用
GCD对于开发者来说,最简单的,就是通过调用dispatch把一连串的异步任务添加到队列中,进行异步执行操作。
代码调用如下:
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
- async表示异步运行;
- queue为我们提前创建的队列;
- block也就是“块”,让我们执行事件的模块;
async(异步)与
sync(同步):
当然,我们也可以使用同步任务,使用dispatch_sync
函数添加到相应的队列中,而这个函数会阻塞当前调用线程,直到相应任务完成执行。
但是,也正因为这样的同步特性,在实际项目中,当有同步任务添加到正在执行同步任务的队列时,串行的队列会出现死锁。而且由于同步任务会阻塞主线程的运行,可能会导致某个事件无法响应。
队列(queue):
需要注意的是,调用dispatch_async不会让块运行,而是把块添加到队列末尾。队列不是线程,它的作用是组织块。(如果读者学过数据结构的知识,就会知道队列的基本特征如饭堂排队队,先到的排前面,先打到饭,也就是“先进先出”原理)
在GCD中,可以给开发者调用的常见公共队列有以下两种:
dispatch_get_global_queue
:用于获取应用全局共享的并发队列 (提供多个线程来执行任务,所以可以按序启动多个任务并发执行。可用于后台执行任务)dispatch_get_main_queue
: 用于获取应用主线程关联的串行调度队列(只提供一个线程执行任务。运行的main主线程,一般用于UI的搭建)
(还有另外一种,dispatch_get_current_queue,
用于获取当前正在执行任务的队列,主要用于调试,但是
在iOS 6.0
之后苹果已经废弃,原因是容易造成死锁。详情可以查看官方注释。)
这两种公共队列的调用便可以解决我们刚刚关于后台执行任务、主线程用于更新UI界面的问题,
结构如下:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 把逻辑计算等需要消耗长时间的任务,放在此处的全局共享的并发队列执行;
dispatch_async(dispatch_get_main_queue(), ^{
// 回到主线程更新UI界面;
});
});
例如在有一些项目中,会涉及到异步下载图片,这个时候就可以使用这样一种结构来进行任务的分配:
// 异步下载图片
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//先把下载数据的任务放在全局共享并发队列中执行
NSURL *url = [NSURL URLWithString:@"图片的URL"];
NSData * data = [[NSData alloc]initWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:data];
if(data != nil)
{
// 完成后,回到主线程显示图片
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.image = image;
});
}
});
二、 串行队列 and 并行队列
1.串行(Serial)的执行:指同一时间每次只能执行一个任务。 线程池只提供一个线程用来执行任务,所以后一个任务必须等到前一个任务执行结束才能开始。
可以添加多个任务到队列中,执行次序FIFO,但是当程序需要执行大量的任务时,虽然系统允许,但是鉴于程序的资源分配,应该交给全局并发队列来完成才能更好地发挥系统性能。
创建串行队列的方式如下:
dispatch_queue_t serialQueue = dispatch_queue_create("zuoA", NULL);
//第一个参数是队列的名称,通常使用公司的反域名;第二个参数是队列相关属性,一般用NULL.
关于什么是FIFO次序,我们用代码解释一下
- (IBAction)SerialQueue:(UIButton *)sender {
dispatch_queue_t serialQueue = dispatch_queue_create("zuoA", NULL);
dispatch_async(serialQueue, ^{
sleep(3);
NSLog(@"A任务");
});
dispatch_async(serialQueue, ^{
sleep(2);
NSLog(@"B任务");
});
dispatch_async(serialQueue, ^{
sleep(1);
NSLog(@"C任务");
});
}
console控制台显示如下:
2016-03-15 15:04:11.909 dispatch_queue的多任务GCD使用[92316:2538875] A任务
2016-03-15 15:04:13.910 dispatch_queue的多任务GCD使用[92316:2538875] B任务
2016-03-15 15:04:14.910 dispatch_queue的多任务GCD使用[92316:2538875] C任务
可以看得到,即使需要等待几秒,后面所添加的任务也必须等待前面的任务完成后才能执行,类似我们前面所讲"饭堂"排队的例子,队列完全按照"先进先出"的顺序,也即是所执行的顺序取决于:开发者将工作任务添加进队列的顺序。
2.并行(concurrent)的执行:可同一时间可以同时执行多个任务。
- 负荷:并发执行任务与系统有关,能够同时执行任务的数量是由系统根据应用和此时的系统状态等动态变化决定的。
- 顺序:由于并行队列也是队列(这是废话T^T),因此每个任务的启动时间也是按照FIFO次序,也就是加入queue的顺序,但是结束的顺序则依赖各自的任务所需要消耗的时间。
所以,与串行的不同的是,虽然启动时间一致,但是这是“并发执行”,因此不需要等到上一个任务完成后才进行下一个任务。所以每个块中的各部分的先后执行的顺序需要视情况而定。
上代码,找不同。。。
- (IBAction)concurrentQueue:(UIButton *)sender { dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(concurrentQueue, ^{ sleep(3); NSLog(@"A任务"); }); dispatch_async(concurrentQueue, ^{ sleep(2); NSLog(@"B任务"); }); dispatch_async(concurrentQueue, ^{ sleep(1); NSLog(@"C任务"); }); }
console控制台显示如下:
2016-03-15 15:02:06.911 dispatch_queue的多任务GCD使用[92294:2537296] C任务 2016-03-15 15:02:07.907 dispatch_queue的多任务GCD使用[92294:2537147] B任务 2016-03-15 15:02:08.908 dispatch_queue的多任务GCD使用[92294:2537177] A任务
通过控制台左边的时间记录,可以看到,与串行队列不同的是,并行队列中这3个任务的并行启用,与串行不同的是,不需要等到A任务调用完,就已经在调用B、C,显著地提高了线程的执行速度,凸显了并行队列所执行的异步操作的并行特性;
另外,从这段代码中,不同的是串行队列需要创建一个新的队列,而并行队列中,只需要调用iOS系统中为我们提供的全局共享dispatch_get_global_queue就可以了:
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
第一个参数为iOS系统为全局共享队列提供4种调度的方式,主要区别即是优先级的不同而已:
- DISPATCH_QUEUE_PRIORITY_HIGH
- DISPATCH_QUEUE_PRIORITY_DEFAULT
- DISPATCH_QUEUE_PRIORITY_LOW
- DISPATCH_QUEUE_PRIORITY_BACKGROUND
我们采用默认的DISPATCH_QUEUE_PRIORITY_DEFAULT方式,而右边的第二个参数是苹果预留的,暂时没有其他的含义,所以,一般默认为:0。
并发的好处就是不需要像串行一样按照顺序执行,并发执行可以显著地提高速度。
三、dispatch_group_async的使用
有时候,我们会遇到这样的情况,UI界面部分的显示,需要在完成几个任务再进行主任务,例如3张图片下载完毕,才通知UI界面已经完成任务。
我们可以通过分派组(dispatch group)进行并发程序块分配的运用,将异步分派(dispatch_async)的所有程序块设置为松散,或者分配给多个线程来执行,监听到这组任务全部完成后,使用dispatch_group_notify()通知并调用notify中的块,例如UI界面的程序块。
代码:
- (IBAction)groupQueue:(UIButton *)sender {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
sleep(3);
NSLog(@"A任务");
});
dispatch_group_async(group, queue, ^{
sleep(2);
NSLog(@"B任务");
});
//group组中的任务完后,通知并调用notify中的块
dispatch_group_notify(group, queue, ^{
NSLog(@"主任务");
});
}
console控制台显示如下:
2016-03-16 11:18:41.306 dispatch_queue的多任务GCD使用[94865:2718342] B任务
2016-03-16 11:18:42.302 dispatch_queue的多任务GCD使用[94865:2718341] A任务
2016-03-16 11:18:42.303 dispatch_queue的多任务GCD使用[94865:2718341] 主任务
结果验证了前面说的,直到分派组任务都完后,notify添加的任务块才会执行。
眼尖的读者可能也发现,整个任务组完成的时间比2个任务分别运行的时间还要短!这得益于我们同时进行了两种计算~
当然在真实的开发运用中,这种明显运行时间缩短的效果,取决于所需要执行的工作量和可用的资源,以及多个CPU核心的可用性,因此在多核技术日益完善的大环境下,这样一种多线程技术将得到更有效的利用。
四、dispatch_barrier_async的使用
dispatch_barrier(分派屏障)是当前面的任务执行完后,才执行barrier块的任务,而且后面的任务也得等到barrier块的执行完毕后才能开始执行。
很好地突显了“障碍物”这样的特性,那么代码上应该怎么写呢?
按照并发的性质,我们在barrierQueue方法中敲入以下代码:
- (IBAction)barrierQueue:(UIButton *)sender {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
sleep(2);
NSLog(@"A任务");
});
dispatch_async(queue, ^{
sleep(1);
NSLog(@"B任务");
});
dispatch_barrier_async(queue, ^{
NSLog(@"barrier任务");
});
dispatch_async(queue, ^{
sleep(1);
NSLog(@"C任务");
});
}
console控制台显示如下:
2016-03-16 13:18:47.525 dispatch_queue的多任务GCD使用[95191:2752854] barrier任务
2016-03-16 13:18:48.529 dispatch_queue的多任务GCD使用[95191:2752839] B任务
2016-03-16 13:18:48.529 dispatch_queue的多任务GCD使用[95191:2752844] C任务
2016-03-16 13:18:49.528 dispatch_queue的多任务GCD使用[95191:2752840] A任务
任务的执行顺序依然是跟并行队列的方法一样,barrier没有发挥它的“障碍物”的界限作用。这是因为barrier这一块是依赖队列queue的模型来执行的,当队列为全局共享时,barrier就无法发挥其作用。我们需要新创建一个队列,
dispatch_queue_t queue = dispatch_queue_create("zuoA", DISPATCH_QUEUE_SERIAL);
完整的程序如下:
- (IBAction)barrierQueue:(UIButton *)sender {
dispatch_queue_t queue = dispatch_queue_create("zuoA", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
sleep(2);
NSLog(@"A任务");
});
dispatch_async(queue, ^{
sleep(1);
NSLog(@"B任务");
});
dispatch_barrier_async(queue, ^{
NSLog(@"barrier任务");
});
dispatch_async(queue, ^{
sleep(1);
NSLog(@"C任务");
});
}
console控制台显示如下:
2016-03-16 13:30:14.251 dispatch_queue的多任务GCD使用[95263:2759658] A任务
2016-03-16 13:30:15.255 dispatch_queue的多任务GCD使用[95263:2759658] B任务
2016-03-16 13:30:15.255 dispatch_queue的多任务GCD使用[95263:2759658] barrier任务
2016-03-16 13:30:16.256 dispatch_queue的多任务GCD使用[95263:2759658] C任务
这就是我们想要得到的效果:确实只有在前面A、B任务完成后,barrier任务才能执行,最后才能执行C任务。
那么,dispatch_queue_create为什么要用 DISPATCH_QUEUE_SERIAL,可以用其他么?答案是肯定的。把参数换成DISPATCH_QUEUE_CONCURRENT
可以得到以下输出:
2016-03-16 13:34:23.855 dispatch_queue的多任务GCD使用[95294:2762604] B任务
2016-03-16 13:34:24.853 dispatch_queue的多任务GCD使用[95294:2762603] A任务
2016-03-16 13:34:24.853 dispatch_queue的多任务GCD使用[95294:2762603] barrier任务
2016-03-16 13:34:25.856 dispatch_queue的多任务GCD使用[95294:2762603] C任务
也就是说,A、B、C任务完全是按照队列的顺序执行,只是由于barrier块的“屏障”作用,把A、B任务放在前面,而使得后来加入的C任务只有等到barrier块执行完毕才能运行;
五、dispatch_suspend(暂停)和 dispatch_resume(继续)
- 暂停:当需要暂停某个队列queue时, 调用dispatch_suspend(queue),此时阻止了queue执行块对象,且
queue
的引用计数增加; - 继续:继续queue时,调用
dispatch_resume(queue),此时queue启动执行块的操作,
queue
的引用计数减少;
需要注意的是,suspend与resume是异步的,只在block块之间调用,而且必须是成对存在的。
还有一些其他的dispatch函数,例如
dispatch_once:可以使特定的块在整个应用程序生命周期中只被执行一次~(在单例模式中使用到.)
dispatch_apply:执行某个代码片段n次(开发者可以自己设定)。
dispatch_after:当我们需要等待几秒后进行某个操作,可以使用这个函数;
注意事项:
1.在上面的例子中,我们没有使用过手动内管其内存,因为系统会自动管理。
如果你部署的最低目标低于 iOS 6.0 or Mac OS X 10.8
这个有兴趣的童鞋可以了解一下。