GCD 英文全称是 Grand Cental Dispatch(翻译成中文有点别扭:牛 X 的中枢调度器)。它是 Apple 开发的一个多核编程的较新的解决方法,主要是用于优化应用程序以支持多核处理器以及其他对称多处理系统,是一个在线程池模式的基础上执行的并行任务。纯 C 语言,提供了非常多而强大的函数。

GCD 概述

GCD 有以下优势:

  • GCD 是 Apple 公司为多核的并行运算提出的解决方案。
  • GCD 会自动利用更多的 CPU 内核(比如双核、四核,比如 iPhone6 使用的 A8 处理器是一款双核处理器)。
  • GCD 会自动管理线程的生命周期(包括创建线程、调度任务、销毁线程)。
  • 程序🐶只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码。

在 GCD 中,有两个比较核心的概念:

  • 任务:执行什么操作(比如下载图片,打印一千次 “我爱你” 等等)。
  • 队列:用来存放任务的。

因此,在使用 GCD 的时候,就两个步骤:

  • 定制一个或者多个任务,确定想要做的操作
  • 将这些任务添加到队列中,这样 GCD 会自动将队列中的任务取出,然后放到对应的线程中执行。注意:任务的取出遵循队列的 FIFO(First In First Out)原则,先进先出,后进后出。

好,先说任务这一块,在 GCD 中有两个用来执行任务的函数(也就是用来定制任务的):

  • 同步的方式执行任务
1
2
3
// queue:存放任务的队列
// block:在这个 block 里执行任务
dispatch_sync(<#dispatch_queue_t _Nonnull queue#>, <#^(void)block#>)
  • 异步的方式执行任务
1
2
3
// queue:存放任务的队列
// block:在这个 block 里执行任务
dispatch_async(<#dispatch_queue_t _Nonnull queue#>, <#^(void)block#>)

同步和异步区别:

  • 同步:只能在当前线程中执行任务,不具备开启线程的能力。
  • 异步:可以在新的线程中执行任务,具备开启新线程的能力。

好,上面说到了有两个用来执行任务的函数,其中一个参数 block 是用来执行任务的,另一个参数 queue 是用来存放任务的队列。在 GCD 中,队列可以分为两种类型:

  • 并发队列(Concurrent Dispatch Queue)
    • 允许让多个任务并发(同时)执行,会自动开启多个线程同时执行任务。
    • 并发功能只有在异步函数(dispatch_async)中才生效,在同步函数(dispatch_sync)中不生效。
  • 串行队列(Serial Dispatch Queue)
    • 让任务一个接着一个地执行,即一个任务执行完毕后,再执行下一个任务。

至此,说了 4 个比较容易混淆的术语:同步、异步,并发、串行

  • 同步和异步主要影响:能不能开启新线程。
    • 同步:只能在当前线程中执行任务,不具备开启线程的能力。
    • 异步:可以在新的线程中执行任务,具备开启新线程的能力(如果异步函数用在主队列上就开不了线程了)。
  • 并发和串行主要影响:任务的执行方式,并不影响能不能开启线程。
    • 并发:允许让多个任务并发(同时)执行。
    • 串行:让任务一个接着一个地执行。

举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
- (void)viewDidLoad {
[super viewDidLoad];
// 1. 创建并发队列
// 第一个参数是指该队列的名称(唯一的,约定俗成以域名开头)
// 第二个参数是指并发队列(DISPATCH_QUEUE_CONCURRENT)还是串行队列(DISPATCH_QUEUE_SERIAL)
dispatch_queue_t queue = dispatch_queue_create("me.angelen.yayaya.download.image", DISPATCH_QUEUE_CONCURRENT);
// 2. 将任务添加到这个并发队列中
dispatch_async(queue, ^{
// 任务 1
for (NSInteger i = 0; i < 5; i++) {
NSLog(@"01 - i = %zd, %@", i, [NSThread currentThread]);
}
});
dispatch_async(queue, ^{
// 任务 2
for (NSInteger i = 0; i < 5; i++) {
NSLog(@"02 - i = %zd, %@", i, [NSThread currentThread]);
}
});
dispatch_async(queue, ^{
// 任务 3
for (NSInteger i = 0; i < 5; i++) {
NSLog(@"03 - i = %zd, %@", i, [NSThread currentThread]);
}
});
}

上面代码中就是将任务 1、2、3 以 FIFO 原则添加到 queue 这个并发队列中,因为是异步函数,因此可以开启新线程(可以执行查看效果,注意,主线程的 number = 1,number = 2、3、4 都是子线程,虽然任务的取出遵循 FIFO,任务 1 先添加,但是并不一定先执行完,所以有可能任务 3 执行完了然后任务 2 执行完然后任务 1 执行完):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
01 - i = 0, <NSThread: 0x7f959b70acf0>{number = 2, name = (null)}
02 - i = 0, <NSThread: 0x7f959b5a40f0>{number = 3, name = (null)}
03 - i = 0, <NSThread: 0x7f959b70fa90>{number = 4, name = (null)}
02 - i = 1, <NSThread: 0x7f959b5a40f0>{number = 3, name = (null)}
01 - i = 1, <NSThread: 0x7f959b70acf0>{number = 2, name = (null)}
03 - i = 1, <NSThread: 0x7f959b70fa90>{number = 4, name = (null)}
02 - i = 2, <NSThread: 0x7f959b5a40f0>{number = 3, name = (null)}
01 - i = 2, <NSThread: 0x7f959b70acf0>{number = 2, name = (null)}
03 - i = 2, <NSThread: 0x7f959b70fa90>{number = 4, name = (null)}
02 - i = 3, <NSThread: 0x7f959b5a40f0>{number = 3, name = (null)}
01 - i = 3, <NSThread: 0x7f959b70acf0>{number = 2, name = (null)}
02 - i = 4, <NSThread: 0x7f959b5a40f0>{number = 3, name = (null)}
03 - i = 3, <NSThread: 0x7f959b70fa90>{number = 4, name = (null)}
01 - i = 4, <NSThread: 0x7f959b70acf0>{number = 2, name = (null)}
03 - i = 4, <NSThread: 0x7f959b70fa90>{number = 4, name = (null)}

异步函数 + 并发队列

以上这个例子就是 异步函数 + 并发队列,实现的效果就是:可以同时开启多个线程。在以上代码中,我们是自己创建并发队列的,但是实际开发中,一般不需要自己创建并发队列,而是可以直接使用 GCD 默认提供给我们的并发队列。

GCD 默认已经提供了全局的并发队列,供整个应用使用,不需要手动创建。使用

1
2
3
// identifier:队列的优先级
// flags:Flags that are reserved for future use. Always specify 0 for this parameter.
dispatch_get_global_queue(<#long identifier#>, <#unsigned long flags#>)

函数获得全局的并发队列,其中 identifier 参数是表示队列的优先级,有一下几种(看英文解析):

  • QOS_CLASS_USER_INTERACTIVE: The user interactive class represents tasks that need to be done immediately in order to provide a nice user experience. Use it for UI updates, event handling and small workloads that require low latency. The total amount of work done in this class during the execution of your app should be small.(用户交互,希望尽快完成,用户对结果很期望,不要放太多耗时的操作)
  • QOS_CLASS_USER_INITIATED: The user initiated class represents tasks that are initiated from the UI and can be performed asynchronously. It should be used when the user is waiting for immediate results, and for tasks required to continue user interaction.(用户期望,不要放太耗时的操作)
  • QOS_CLASS_UTILITY: The utility class represents long-running tasks, typically with a user-visible progress indicator. Use it for computations, I/O, networking, continous data feeds and similar tasks. This class is designed to be energy efficient.(实用工具,耗时操作可以使用这个选项)
  • QOS_CLASS_BACKGROUND: The background class represents tasks that the user is not directly aware of. Use it for prefetching, maintenance, and other tasks that don’t require user interaction and aren’t time-sensitive.(后台)
  • QOS_CLASS_DEFAULT:

注意,在 iOS 7.0 或之前的版本,是使用

1
2
3
// priority:队列的优先级
// flags:Flags that are reserved for future use. Always specify 0 for this parameter.
dispatch_get_global_queue(dispatch_queue_priority_t priority, unsigned long flags);

函数来获得全局的并发队列的,其中 priority 参数是表示队列的优先级,有一下几种:

1
2
3
4
#define DISPATCH_QUEUE_PRIORITY_HIGH 2 // 高
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0 // 默认(中)
#define DISPATCH_QUEUE_PRIORITY_LOW (-2) // 低
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN // 后台

通过官方文档的注释可以看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/*!
* @function dispatch_get_global_queue
*
* @abstract
* Returns a well-known global concurrent queue of a given quality of service
* class.
*
* @discussion
* The well-known global concurrent queues may not be modified. Calls to
* dispatch_suspend(), dispatch_resume(), dispatch_set_context(), etc., will
* have no effect when used with queues returned by this function.
*
* @param identifier
* A quality of service class defined in qos_class_t or a priority defined in
* dispatch_queue_priority_t.
*
* It is recommended to use quality of service class values to identify the
* well-known global concurrent queues:
* - QOS_CLASS_USER_INTERACTIVE
* - QOS_CLASS_USER_INITIATED
* - QOS_CLASS_DEFAULT
* - QOS_CLASS_UTILITY
* - QOS_CLASS_BACKGROUND
*
* The global concurrent queues may still be identified by their priority,
* which map to the following QOS classes:
* - DISPATCH_QUEUE_PRIORITY_HIGH: QOS_CLASS_USER_INITIATED
* - DISPATCH_QUEUE_PRIORITY_DEFAULT: QOS_CLASS_DEFAULT
* - DISPATCH_QUEUE_PRIORITY_LOW: QOS_CLASS_UTILITY
* - DISPATCH_QUEUE_PRIORITY_BACKGROUND: QOS_CLASS_BACKGROUND
*
* @param flags
* Reserved for future use. Passing any value other than zero may result in
* a NULL return value.
*
* @result
* Returns the requested global queue or NULL if the requested global queue
* does not exist.
*/
__OSX_AVAILABLE_STARTING(__MAC_10_6,__IPHONE_4_0)
DISPATCH_EXPORT DISPATCH_CONST DISPATCH_WARN_RESULT DISPATCH_NOTHROW
dispatch_queue_t
dispatch_get_global_queue(long identifier, unsigned long flags);

同步函数 + 并发队列

同步函数 + 并发队列 实现的效果就是:不会开启新线程,在当前线程运行,因此也没有并发功能。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_queue_t queue = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0);
dispatch_sync(queue, ^{
// 任务 1
for (NSInteger i = 0; i < 5; i++) {
NSLog(@"01 - i = %zd, %@", i, [NSThread currentThread]);
}
});
dispatch_sync(queue, ^{
// 任务 2
for (NSInteger i = 0; i < 5; i++) {
NSLog(@"02 - i = %zd, %@", i, [NSThread currentThread]);
}
});
dispatch_sync(queue, ^{
// 任务 3
for (NSInteger i = 0; i < 5; i++) {
NSLog(@"03 - i = %zd, %@", i, [NSThread currentThread]);
}
});
}

运行效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
01 - i = 0, <NSThread: 0x7fe7d2407820>{number = 1, name = main}
01 - i = 1, <NSThread: 0x7fe7d2407820>{number = 1, name = main}
01 - i = 2, <NSThread: 0x7fe7d2407820>{number = 1, name = main}
01 - i = 3, <NSThread: 0x7fe7d2407820>{number = 1, name = main}
01 - i = 4, <NSThread: 0x7fe7d2407820>{number = 1, name = main}
02 - i = 0, <NSThread: 0x7fe7d2407820>{number = 1, name = main}
02 - i = 1, <NSThread: 0x7fe7d2407820>{number = 1, name = main}
02 - i = 2, <NSThread: 0x7fe7d2407820>{number = 1, name = main}
02 - i = 3, <NSThread: 0x7fe7d2407820>{number = 1, name = main}
02 - i = 4, <NSThread: 0x7fe7d2407820>{number = 1, name = main}
03 - i = 0, <NSThread: 0x7fe7d2407820>{number = 1, name = main}
03 - i = 1, <NSThread: 0x7fe7d2407820>{number = 1, name = main}
03 - i = 2, <NSThread: 0x7fe7d2407820>{number = 1, name = main}
03 - i = 3, <NSThread: 0x7fe7d2407820>{number = 1, name = main}
03 - i = 4, <NSThread: 0x7fe7d2407820>{number = 1, name = main}

异步函数 + 串行队列

异步函数 + 串行队列 实现的效果就是:会开启新线程,但是因为任务 1、2、3 是添加到串行队列的,因此只开一条线程,遵循 FIFO 原则,执行完一个任务再执行下一个任务 。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- (void)viewDidLoad {
[super viewDidLoad];
// 串行队列没有全局的,只能自己创建(DISPATCH_QUEUE_SERIAL 就是 NULL)
dispatch_queue_t queue = dispatch_queue_create("me.angelen.xxx.yyy", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
// 任务 1
for (NSInteger i = 0; i < 5; i++) {
NSLog(@"01 - i = %zd, %@", i, [NSThread currentThread]);
}
});
dispatch_async(queue, ^{
// 任务 2
for (NSInteger i = 0; i < 5; i++) {
NSLog(@"02 - i = %zd, %@", i, [NSThread currentThread]);
}
});
dispatch_async(queue, ^{
// 任务 3
for (NSInteger i = 0; i < 5; i++) {
NSLog(@"03 - i = %zd, %@", i, [NSThread currentThread]);
}
});
}

运行效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
01 - i = 0, <NSThread: 0x7f8e506081b0>{number = 2, name = (null)}
01 - i = 1, <NSThread: 0x7f8e506081b0>{number = 2, name = (null)}
01 - i = 2, <NSThread: 0x7f8e506081b0>{number = 2, name = (null)}
01 - i = 3, <NSThread: 0x7f8e506081b0>{number = 2, name = (null)}
01 - i = 4, <NSThread: 0x7f8e506081b0>{number = 2, name = (null)}
02 - i = 0, <NSThread: 0x7f8e506081b0>{number = 2, name = (null)}
02 - i = 1, <NSThread: 0x7f8e506081b0>{number = 2, name = (null)}
02 - i = 2, <NSThread: 0x7f8e506081b0>{number = 2, name = (null)}
02 - i = 3, <NSThread: 0x7f8e506081b0>{number = 2, name = (null)}
02 - i = 4, <NSThread: 0x7f8e506081b0>{number = 2, name = (null)}
03 - i = 0, <NSThread: 0x7f8e506081b0>{number = 2, name = (null)}
03 - i = 1, <NSThread: 0x7f8e506081b0>{number = 2, name = (null)}
03 - i = 2, <NSThread: 0x7f8e506081b0>{number = 2, name = (null)}
03 - i = 3, <NSThread: 0x7f8e506081b0>{number = 2, name = (null)}
03 - i = 4, <NSThread: 0x7f8e506081b0>{number = 2, name = (null)}

使用场景:比如有几个耗时的操作,需要执行完一个任务再去执行下一个任务,这个时候,为了不阻塞主线程,那么就要使用异步函数了,加上是需要执行完一个任务才能去执行下一个任务,因此可以把这些任务按照顺序添加到串行队列里。

同步函数 + 串行队列

同步函数 + 串行队列 实现的效果就是:不会开启新线程,在当前线程运行,执行完一个任务再执行下一个任务。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (void)viewDidLoad {
[super viewDidLoad];
// 串行队列没有全局的,只能自己创建(DISPATCH_QUEUE_SERIAL 就是 NULL)
dispatch_queue_t queue = dispatch_queue_create("me.angelen.xxx.yyy", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
// 任务 1
for (NSInteger i = 0; i < 5; i++) {
NSLog(@"01 - i = %zd, %@", i, [NSThread currentThread]);
}
});
dispatch_sync(queue, ^{
// 任务 2
for (NSInteger i = 0; i < 5; i++) {
NSLog(@"02 - i = %zd, %@", i, [NSThread currentThread]);
}
});
dispatch_sync(queue, ^{
// 任务 3
for (NSInteger i = 0; i < 5; i++) {
NSLog(@"03 - i = %zd, %@", i, [NSThread currentThread]);
}
});
}

运行效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
01 - i = 0, <NSThread: 0x7fa85ae03af0>{number = 1, name = main}
01 - i = 1, <NSThread: 0x7fa85ae03af0>{number = 1, name = main}
01 - i = 2, <NSThread: 0x7fa85ae03af0>{number = 1, name = main}
01 - i = 3, <NSThread: 0x7fa85ae03af0>{number = 1, name = main}
01 - i = 4, <NSThread: 0x7fa85ae03af0>{number = 1, name = main}
02 - i = 0, <NSThread: 0x7fa85ae03af0>{number = 1, name = main}
02 - i = 1, <NSThread: 0x7fa85ae03af0>{number = 1, name = main}
02 - i = 2, <NSThread: 0x7fa85ae03af0>{number = 1, name = main}
02 - i = 3, <NSThread: 0x7fa85ae03af0>{number = 1, name = main}
02 - i = 4, <NSThread: 0x7fa85ae03af0>{number = 1, name = main}
03 - i = 0, <NSThread: 0x7fa85ae03af0>{number = 1, name = main}
03 - i = 1, <NSThread: 0x7fa85ae03af0>{number = 1, name = main}
03 - i = 2, <NSThread: 0x7fa85ae03af0>{number = 1, name = main}
03 - i = 3, <NSThread: 0x7fa85ae03af0>{number = 1, name = main}
03 - i = 4, <NSThread: 0x7fa85ae03af0>{number = 1, name = main}

重要提一下:串行队列的获取有两种方式,除了通过

1
dispatch_queue_create("me.angelen.xxx.yyy", DISPATCH_QUEUE_SERIAL);

这个函数来创建以外,还可以使用主队列(跟主线程相关联的队列)

  • 主队列是 GCD 自带的一种特殊的串行队列。
  • 放在主队列的任务,都会放到主线程(UI 线程)去执行。

可以通过以下函数获取主队列:

1
dispatch_get_main_queue()

异步函数 + 主队列

异步函数 + 主队列 实现的效果就是:遵循 FIFO 原则在主线程上执行任务,不开启新线程,不存在异步的概念。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (void)viewDidLoad {
[super viewDidLoad];
// 获取主队列
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
// 任务 1
for (NSInteger i = 0; i < 2; i++) {
NSLog(@"01 - i = %zd, %@", i, [NSThread currentThread]);
}
});
dispatch_async(queue, ^{
// 任务 2
for (NSInteger i = 0; i < 2; i++) {
NSLog(@"02 - i = %zd, %@", i, [NSThread currentThread]);
}
});
dispatch_async(queue, ^{
// 任务 3
for (NSInteger i = 0; i < 2; i++) {
NSLog(@"03 - i = %zd, %@", i, [NSThread currentThread]);
}
});
}

运行效果:

1
2
3
4
5
6
01 - i = 0, <NSThread: 0x7fe4c1504b90>{number = 1, name = main}
01 - i = 1, <NSThread: 0x7fe4c1504b90>{number = 1, name = main}
02 - i = 0, <NSThread: 0x7fe4c1504b90>{number = 1, name = main}
02 - i = 1, <NSThread: 0x7fe4c1504b90>{number = 1, name = main}
03 - i = 0, <NSThread: 0x7fe4c1504b90>{number = 1, name = main}
03 - i = 1, <NSThread: 0x7fe4c1504b90>{number = 1, name = main}

同步函数 + 主队列

同步函数 + 主队列 实现的效果就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)viewDidLoad {
[super viewDidLoad];
// 获取主队列
dispatch_queue_t queue = dispatch_get_main_queue();
NSLog(@"viewDidLoad begin.");
dispatch_sync(queue, ^{
// 任务 1
for (NSInteger i = 0; i < 2; i++) {
NSLog(@"01 - i = %zd, %@", i, [NSThread currentThread]);
}
});
dispatch_sync(queue, ^{
// 任务 2
for (NSInteger i = 0; i < 2; i++) {
NSLog(@"02 - i = %zd, %@", i, [NSThread currentThread]);
}
});
NSLog(@"viewDidLoad end.");
}

发现运行时仅仅打印:

1
viewDidLoad begin.

而没有打印任务 1 和任务 2,这是为什么呢?我们假设执行 viewDidLoad 这个操作为 主任务,而且我们知道,它是在 UI 线程上执行的。

  • 首先,主任务 被添加到主队列中,在执行 主任务 的时候,任务 1 被添加到主队列中;
  • 此时,主任务 继续往下执行,发现要先执行完 任务 1 才能继续往下,但是 任务 1 是添加在 主任务 后面的,得等 主任务 执行完毕之后,才能执行 任务 1
  • 所以,就导致了先要执行完 主任务,先得执行完 任务 1,想要执行完 任务 1,又得先执行完 主任务。导致了一个死循环,不能继续下去。

我想,这个就是所谓的 死锁 吧~

同步函数向主队列提交任务,会造成主线程死锁,因为主队列是一个串行队列,向队列提交任务也是一个任务(提交的任务执行完成,”提交“ 这个任务才算完成),如果同步函数向串行队列提交任务,需要 “提交” 和 “任务” 两个任务同时执行,而串行队列只能执行一个任务,就会造成死锁。

分析一下,以下代码也会导致 死锁

1
2
3
4
5
6
7
8
9
10
11
dispatch_queue_t queue = dispatch_queue_create("me.angelen.xxx.yyy", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
// 任务 1
NSLog(@"01 - %@", [NSThread currentThread]);
dispatch_sync(queue, ^{
// 任务 2
NSLog(@"02 - %@", [NSThread currentThread]);
});
});

总结:各种队列的执行效果

各种队列的执行效果

注意:使用 sync 函数往当前串行队列(主队列也是串行队列)中添加任务,会卡住当前的串行队列。

线程间通信

从子线程回到主线程,可以这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
// 在子线程这里执行耗时的操作
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"https://s3.amazonaws.com/sitebuilderreport-assets/stock_photos/files/000/028/295/small/tumblr_oeyy3mHBQc1v4yqceo1_500.jpg?1476396054"]];
UIImage *image = [UIImage imageWithData:data];
dispatch_async(dispatch_get_main_queue(), ^{
// 在这里回到主线程(UI 线程),执行 UI 刷新操作
self.imageView.image = image;
});
});
}

GCD 的常用方法

  1. 延时执行:iOS 中常见的延时执行有 3 种方式。
  • 使用 performSelector
1
[self performSelector:@selector(getupHi:) withObject:@"angelen" afterDelay:2.0];
  • 使用 GCD
1
2
3
4
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// code to be executed after a specified delay
NSLog(@"ok");
});
  • 使用 NSTimer
1
2
3
[NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:@selector(showNSLog) userInfo:nil repeats:NO];
// 注意,以下方式也可以延时,但是会阻塞当前线程,慎用!
[NSThread sleepForTimeInterval:2];

这 3 中方式详情可看:http://www.jianshu.com/p/0ea566aabf8f

  1. 一次性代码:使用 dispatch_once 函数能保证某段代码在程序运行过程中只被执行 1 次。
1
2
3
4
5
6
7
8
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只执行 1 次的代码(这里面默认是线程安全的,不用担心多条线程同时抢夺资源)
NSLog(@"我是帅哥");
});
}

注意:只要程序运行过程中,无论这个 didSelectItemAtIndexPath 被点击多少次,或者返回到上个页面再进来点击也好,这句 “我是帅哥” 都只会打印一次。dispatch_once 函数很适合做资源的加载,或者单例等等。

  1. 执行任务次序:使用 dispatch_barrier_async 函数可以让在它前面的任务执行结束后,它才执行,而且后面的任务等它执行完成之后才会执行,例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (void)viewDidLoad {
[super viewDidLoad];
// 创建一个并发队列
dispatch_queue_t queue = dispatch_queue_create("me.angelen.yayaya", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"1");
});
dispatch_async(queue, ^{
NSLog(@"2");
});
dispatch_barrier_async(queue, ^{
NSLog(@"dispatch_barrier_async");
});
dispatch_async(queue, ^{
NSLog(@"3");
});
dispatch_async(queue, ^{
NSLog(@"4");
});
}

运行效果,1 和 2 会在 dispatch_barrier_async 前打印,3 和 4 会在 dispatch_barrier_async 后打印,1、2 次序不固定,3、4 次序不固定。

1
2
3
4
5
2
1
dispatch_barrier_async
3
4

不过注意:这个 queue 不能是全局的并发队列,即 dispatch_get_global_queue。

  1. 快速迭代

我们平时在迭代的时候一般使用 for 循环:

1
2
3
for (NSInteger i = 0; i < 5; i++) {
NSLog(@"i = %zd", i);
}

这种迭代是按照顺序迭代的,我们可以使用

1
dispatch_apply(<#size_t iterations#>, <#dispatch_queue_t queue#>, <#^(size_t)block#>)

函数来进行快速迭代:

1
2
3
4
dispatch_apply(5, dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^(size_t i) {
// 执行 5 次代码,i 顺序不确定
NSLog(@"i = %zd, %@", i, [NSThread currentThread]);
});

运行效果,可以看到它会利用多线程来高效迭代,因为是并发队列(相当于同时做),因此次序会不一样:

1
2
3
4
5
i = 0, <NSThread: 0x7ff923409240>{number = 1, name = main}
i = 1, <NSThread: 0x7ff923628540>{number = 3, name = (null)}
i = 3, <NSThread: 0x7ff923544a60>{number = 5, name = (null)}
i = 2, <NSThread: 0x7ff923453890>{number = 4, name = (null)}
i = 4, <NSThread: 0x7ff923409240>{number = 1, name = main}

比如我们写一个拷贝文件的功能,把一个文件夹的所有文件拷贝到另外一个文件夹里,在我们学习 GCD 之前,我们可能会这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)viewDidLoad {
[super viewDidLoad];
NSString *fromPath = @"/Users/apple/ANGELEN/Games";
NSString *toPath = @"/Users/apple/ANGELEN/GamesCopy";
NSFileManager *fileManager = [NSFileManager defaultManager];
NSArray<NSString *> *fromSubpaths = [fileManager subpathsAtPath:fromPath];
NSLog(@"fromSubpaths = %@", fromSubpaths);
[fromSubpaths enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSString *fromAbsolutePath = [fromPath stringByAppendingPathComponent:obj];
NSString *toAbsolutePath = [toPath stringByAppendingPathComponent:obj];
[fileManager copyItemAtPath:fromAbsolutePath toPath:toAbsolutePath error:nil];
}];
}

运行效果:

1
2
3
4
5
fromSubpaths = (
".DS_Store",
"KingdomRushHD21.dmg", // 这个文件 294.3MB
"Samorost313 Cr .dmg" // 这个文件 976.4MB
)

可以使用 dispatch_apply 来进行可快速迭代,它会适合的开多个线程来处理这个拷贝任务,让这些拷贝任务可以同时执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)viewDidLoad {
[super viewDidLoad];
NSString *fromPath = @"/Users/apple/ANGELEN/Games";
NSString *toPath = @"/Users/apple/ANGELEN/GamesCopy";
NSFileManager *fileManager = [NSFileManager defaultManager];
NSArray<NSString *> *fromSubpaths = [fileManager subpathsAtPath:fromPath];
NSLog(@"fromSubpaths = %@", fromSubpaths);
dispatch_apply(fromSubpaths.count, dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^(size_t idx) {
NSString *obj = fromSubpaths[idx];
NSString *fromAbsolutePath = [fromPath stringByAppendingPathComponent:obj];
NSString *toAbsolutePath = [toPath stringByAppendingPathComponent:obj];
[fileManager copyItemAtPath:fromAbsolutePath toPath:toAbsolutePath error:nil];
});
}

GCD 队列组

很多时候,我们希望异步执行两个比较耗时的任务,都这两个任务都执行完的时候,再做一些处理,这个时候就需要使用到 队列组。比如我们希望从服务端下载两张比较大的图片,当图片都下载完毕之后将这两张图片进行合并:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
- (void)viewDidLoad {
[super viewDidLoad];
UIImageView *imageView = [[UIImageView alloc] initWithFrame:self.view.bounds];
imageView.backgroundColor = [UIColor lightGrayColor];
[self.view addSubview:imageView];
__block UIImage *image1 = nil;
__block UIImage *image2 = nil;
dispatch_queue_t queue = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
// 任务 1:下载第 1 张图片
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"http://cdn.barnimages.com/wp-content/uploads/2016/10/2016-10-21-barnimages-igor-trepeshchenok-01-768x512@2x.jpg"]];
image1 = [UIImage imageWithData:data];
});
dispatch_group_async(group, queue, ^{
// 任务 2:下载第 2 张图片
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"http://cdn.barnimages.com/wp-content/uploads/2016/10/2016-10-19-barnimages-igor-trepeshchenok-01-768x512@2x.jpg"]];
image2 = [UIImage imageWithData:data];
});
// 当 group 中的任务都完成之后
dispatch_group_notify(group, queue, ^{
NSLog(@"complete: %@, %@", image1, image2);
// 开启新的图形上下文
UIGraphicsBeginImageContext(imageView.frame.size);
// 绘制图片
[image1 drawInRect:CGRectMake(0, 0, imageView.frame.size.width, imageView.frame.size.height / 2)];
[image2 drawInRect:CGRectMake(0, imageView.frame.size.height / 2, imageView.frame.size.width, imageView.frame.size.height / 2)];
// 获取当前上下文的图片
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
// 结束上下文
UIGraphicsEndImageContext();
// 回到主线程,显示已经合成的图片,刷新 UI
dispatch_async(dispatch_get_main_queue(), ^{
imageView.image = image;
});
});
NSLog(@"1111111111111");
}

P.S. 这种功能也可以使用 dispatch_barrier_async 实现,科科。

GCD 实现单例模式

单例模式可以保证在 程序运行过程,一个类只有一个实例,而且该实例易于供外界访问(sharedInstance 类似的方式),从而方便的控制了实例的个数,并且节约系统资源。

使用场合:在整个应用程序中,共享一份资源(这份资源只需要创建初始化 1 次)。比如我们开发用到的 UIApplication,SDWebImageDownloader 等等。

一个小注意点:单例模式在 ARC/MRC 环境下的写法有所不同,需要编写 2 套不同的代码

1
2
3
4
5
#if __has_feature(objc_arc)
// ARC
#else
// MRC
#endif

因为现在几乎大部分项目都是使用了 ARC,因此会比较详细介绍单例模式在 ARC 中的实现。

思路:由于是共享一份资源,而且这份资源只需要创建初始化 1 次,因此,我们可以在当给对象分配内存的时候入手,保证每次拿到的实例的内存地址都是同一个。

一开始尝试过直接重写 NSObject 的 alloc 方法,

1
2
3
4
5
6
7
8
9
static ZLPhotoManager *photoManager;
+ (instancetype)alloc {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
photoManager = [super alloc]; // 发现它的内存地址是 0x0
});
return photoManager;
}

后来才知道,系统调用 alloc 方法,alloc 方法会调用 allocWithZone: 方法,因此,我们应该重写 allocWithZone: 方法而不是 alloc 方法。

好,大概知道这个背景之后,我们就可以去实现在 ARC 中的单例模式了:

ZLPhotoManager.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#import "ZLPhotoManager.h"
@interface ZLPhotoManager()<NSCopying>
@end
@implementation ZLPhotoManager
// 1. 在 .m 文件加上 static 关键字,避免外界直接访问 photoManager,只能在本文件访问。如果去掉了 static 关键字的话,那么别人就可以通过 extern 关键字拿到这个 photoManager 并且将它置为 nil,而且,再也创建不了了(因为 dispatch_once)
static ZLPhotoManager *photoManager;
// 2. 重写 allocWithZone: 方法,在这里创建唯一的实例(注意线程安全,dispatch_once 就是线程安全的)
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
photoManager = [super allocWithZone:zone]; // 父类为其分配内存
});
return photoManager;
}
// 3. 提供 1 个类方法让外界访问唯一的实例(易于供外界访问)
+ (instancetype)sharedManager {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
photoManager = [[self alloc] init]; // 直接调用上面重写的 allocWithZone: 方法,得到实例
});
return photoManager;
}
// 4. 由于对象除了通过 alloc init 创建,还可以通过 copy 来创建,因此要重写 copyWithZone: 方法,保证实例唯一(因为 copy 方法内部还是会调用 copyWithZone: 方法)
- (id)copyWithZone:(NSZone *)zone {
return photoManager; // 因为如果调用 copy/copyWithZone: 方法,就说明 photoManager 肯定已经通过 alloc/allocWithZone:/sharedManager 得到过实例,因此这里不需要再创建
}
@end

ZLPhotoManager.h

1
2
3
4
5
6
7
#import <Foundation/Foundation.h>
@interface ZLPhotoManager : NSObject
+ (instancetype)sharedManager;
@end

这样,一个单例就搞出来了,我们试试调用一下:

1
NSLog(@"%p | %p | %p", [[ZLPhotoManager alloc] init], [ZLPhotoManager sharedManager], [[ZLPhotoManager sharedManager] copy]); // 可以发现,他们的内存地址都一样,都是指向同一块资源

注意:

  • 在 allocWithZone: 使用 dispatch_once 是为了在分配内存的时候,无论外面调用了多少次 alloc init,该对象也只是只会分配一次内存,从根本上控制外界调用的时候,只会分配一次。
  • 在 sharedManager 使用 dispatch_once 是为通过这个方法得到实例化对象时,不需要每次都要重新 init 一次,只需要 init 一次就可以了。

非 GCD 方式实现单例模式

步骤和上面实现的步骤一致,但是注意要手动控制线程安全。

ZLPhotoManager.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#import "ZLPhotoManager.h"
@interface ZLPhotoManager()<NSCopying>
@end
@implementation ZLPhotoManager
static ZLPhotoManager *photoManager;
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
// 保证线程安全
@synchronized (self) {
if (!photoManager) {
photoManager = [super allocWithZone:zone];
}
}
return photoManager;
}
+ (instancetype)sharedManager {
@synchronized (self) {
if (!photoManager) {
photoManager = [[self alloc] init];
}
}
return photoManager;
}
- (id)copyWithZone:(NSZone *)zone {
return photoManager;
}
@end

最后记得参考:

http://www.devtalking.com/articles/read-concurrency-programming-guide-1/

or http://geek.csdn.net/news/detail/60236

https://www.raywenderlich.com/79149/grand-central-dispatch-tutorial-swift-part-1