Objective-C and Swift GCD API  

对于iOS的多线程编程,可以使用C语言开发的POSIX 线程 API,也即pthread,或者Objective-C中提供的对该API的封装NSThread。然而线程管理是一件非常复杂的事,管理不当还会影响程序性能,为了避免开发者可能造成的对线程的糟糕的使用,也为了能更好地利用设备上的多核CPU,苹果在OS X 10.6和iOS4中引入了Grand Central Dispatch (GCD)。对于GCD的描述,在苹果的官方文档中有如下描述:

GCD是异步执行任务的技术之一。它将开发者一般在程序中写的管理线程的代码移到了系统级中实现。你要做的事就是定义要执行的任务,然后把任务添加到合适的队列中。GCD负责创建线程执行任务。因为线程管理是系统级的一部分了,所以GCD可统一管理任务并执行,这样就比以前的线程更有效率。

GCD的执行效率更高,尽管是C语言的API,但是使用起来却很方便,本篇文章将会对我们开发中经常用到的API做一总结,内容比较长,可以在右侧目录找想看的。

其实Objective-C中也提供了对GCD API的封装,就是NSOperationQueue,它提供了一些GCD无法直接做到的特性。下一篇文章会详细的总结一下NSOperationQueue

Dispatch Queue

Dispatch Queue是FIFO队列。队列中放的就是待执行的任务,这些任务用一个个Block语法块进行封装,所以实际放到队列中的是Block语法块。按照同时能执行任务的个数,GCD中有两种队列:串行队列Serial Dispatch Queue和并行队列Concurrent Dispatch Queue。串行队列一次执行只能执行一个任务,下一个任务必须等待当前的任务执行结束,才能开始执行。并行队列可以同时执行多个任务,下一个任务不用等待当前任务执行结束再开始执行,但是任务的开始顺序还是按照先进先出原则,先添加到队列中的任务先开始执行。也就是说GCD用了一个线程来处理串行队列中的任务,用了多个线程来处理并行队列中的任务。但是能并行处理多少个并行队列中的任务,是由当前系统的状态决定的。

创建自定义队列

Objective-C中可以通过dispatch_queue_create创建一个Dispatch Queue。

1
dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create("com.example.gcd.MySerialDispatchQueue", NULL);

第一个参数给该Dispatch Queue指定一个唯一标识,这个标识最好采用反向域名的格式,这样在debug工具中我们可以很容易找到自定义的Dispatch Queue。第二个参数可以设置为DISPATCH_QUEUE_SERIAL或者NULL创建一个串行对类,或者设置为DISPATCH_QUEUE_CONCURRENT创建一个并行队列。

这里要说一下GCD的内存管理,在iOS6及之后的版本,已经将GCD对象加入到了ARC的内存管理中。在这之前,开发者还需要对自己创建的队列,调用dispatch_release进行释放。

在Swift中可以直接通过DispatchQueue的构造函数创建一个Dispatch Queue。

1
let mySerialDispatchQueue: DispatchQueue = DispatchQueue(label: "com.example.gcd.MySerialDispatchQueue")

打开官方文档可以看到,DispatchQueue的构造函数实际上有5个,并且其他4个参数都指定了默认值,所以我们可以像上面那样只传label的值。

1
convenience init(label: String, qos: DispatchQoS = .unspecified, attributes: DispatchQueue.Attributes = [], autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency = .inherit, target: DispatchQueue? = nil)
  • label: 指定Dispatch Queue的唯一标识,通dispatch_queue_create中的label。
  • qos: 指定队列的优先级,在下面会跟Objective-C中的全局队列的优先级一起总结。
  • attributes: 不指定该项,创建的就是串行队列。如果要创建并行队列,可以设置为.concurrent
  • autoreleaseFrequency: 指定block中创建的临时对象的释放策略。设置为.inherits,则该队列跟它的目标队列一样的释放策略。设置为.workItem,则该队列会在执行block之前创建一个autorelease pool,在执行后释放pool中的对象。设置为.never,则该队列不负责创建autorelease pool,交由开发者自行处理。所以最好还是设置为.workItem
  • target:设置目标队列,后面会详细说一下目标队列。

主队列和全局队列

除了直接创建Dispatch Queue,系统还提供了两个标准队列:主队列Main Dispatch Queue和全局队列Global Dispatch Queue。主队列是在主线程中执行的Dispatch Queue,因为主线程只有一个,自然主队列就是串行队列。另一个全局队列是所有应用程序都能够使用的并行队列。全局队列有4个执行优先级,高优先级(High Priority)、默认优先级(Default Priority)、低优先级(Low Priority)、后台优先级(Background Priority)。用于管理全局队列的线程,将各自把其使用的全局队列的优先级作为自己执行任务时的优先级。将系统提供的Dispatch Queue总结如下表:

名称 Dispatch Queue的种类 说明
Main Dispatch Queue Serial Dispatch Queue 主线程执行
Global Dispatch Queue (High Priority) Concurrent Dispatch Queue 执行优先级:高(最高优先)
Global Dispatch Queue (Default Priority) Concurrent Dispatch Queue 执行优先级:默认
Global Dispatch Queue (Low Priority) Concurrent Dispatch Queue 执行优先级:低
Global Dispatch Queue (Background Priority) Concurrent Dispatch Queue 执行优先级:后台

在Objective-C中可以通过dispatch_get_main_queue获取主队列。

1
dispatch_queue_main_t mainDispatchQueue = dispatch_get_main_queue();

Swift中更简单,只要通过DispatchQueue.main就得到了主队列。

1
let mainDispatchQueue: DispatchQueue = DispatchQueue.main

再来看一下全局对列,Objective-C中通过dispatch_get_global_queue获取。

1
dispatch_queue_global_t globalDispatchQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

第一个参数指定优先级,第二个参数是一个保留字段,始终传0即可。在代码中查看它的API定义时可以看到,优先级的取值如下,是宏定义的几个值,所以默认优先级时也可以直接写0。

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
* 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

所以这些宏定义是为了让开发者在使用时只简单地考虑优先级的高低等即可,不需要考虑具体场景再决定用哪个。当然也可以直接使用QOS classes指定优先级。

  • QOS_CLASS_USER_INTERACTIVE 指定为该QOS class的队列负责执行与用户交互相关的任务,比如动画、事件处理、更新UI等,所以有最高优先级。该优先级的队列应该只限于做与用户交互相关的任务,所以在上面优先级的宏定义中并没有将其暴露出来。
  • QOS_CLASS_USER_INITIATED 指定为该QOS class的队列用来执行那些会阻碍用户使用你的App的任务,所以优先级也很高。
  • QOS_CLASS_DEFAULT 默认优先级。
  • QOS_CLASS_UTILITY 指定为该QOS class的队列用于执行那些用户不需要立即得到结果的任务,所以优先级相对较低。
  • QOS_CLASS_BACKGROUND 指定为该QOS class的队列用于执行维护或清理等任务,用户不需要关心其结果。

在Swift中队列的优先级取值就直接对应的上面这些,不再是简单的高或者低了。在创建队列时就可以指定优先级,即qos参数。同样地获取全局队列时也可以指定qos以获取对应优先级的队列,默认为.default

1
2
3
4
// 获取默认优先级队列
let globalDispatchQueue: DispatchQueue = DispatchQueue.global()
// 获取高优先级队列
let globalDispatchQueueHigh: DispatchQueue = DispatchQueue.global(qos: .userInitiated)

qos的取值可为.userInteractiveuserInitiated.default.utility.background.unspecified

目标队列

其实在自定义队列中被调度的所有的block最终都将被放入到系统的全局队列和线程池中。默认情况下会把开发者创建的队列放入到默认优先级的全局队列中。但是也可以给自定义的队列设置一个目标队列,让其执行优先级与该目标队列的执行优先级一致。

不仅能改变优先级,如果一个队列是并行的,但是其目标队列是串行的,那么实际上这个队列也会转换为串行队列。再者,不同串行队列中的任务是可以同时执行的,如果把这些串行队列的目标队列都设置为同一个串行队列,那这些串行队列中的任务将不会并行执行。ObjC中国这篇文章中的图,我稍稍调整了一下感觉会更合适。经过测试,如果自定义的串行队列的目标队列是并行队列,还是能保证该串行队列中的block是被串行调度的,所以目标队列设置为串行队列时才显得更有意义。

在Objective-C中通过dispatch_set_target_queue给一个队列设置其目标队列。

1
2
3
dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create("com.example.gcd.MySerialDispatchQueue", NULL);
dispatch_queue_global_t globalDispatchQueueLow = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
dispatch_set_target_queue(mySerialDispatchQueue, globalDispatchQueueLow);

尽快自定义的队列中的任务最终都会放到全局队列中,但是为一个类创建它自己的队列而不是使用全局队列仍普遍被认为是一种好的风格。因为你可以给自定义的队列设置一个唯一的label,这在debug时非常有帮助。

任务调度

Dispatch Queue中任务的执行,通常分异步和同步,异步和同步说的是当前的线程是否要等待队列中的任务执行结束,再接着执行后面的语句。如果是异步,当前的线程不用等待任务执行的结果,继续处理后面的语句。如果是同步,当前的线程要等待任务执行结束,才能再接着执行后面的语句。

添加任务到队列

在Objective-C中可以通过dispatch_async函数异步将block追加到队列中,异步说的是dispatch_async不会阻塞调用它的线程,调用它的线程不会等它执行结束才能执行后面的语句。

1
2
3
4
5
dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create("com.example.gcd.MySerialDispatchQueue", NULL);

dispatch_async(mySerialDispatchQueue, ^{
// 在队列中要执行的语句
});

在Swift中对应的是DispatchQueue的实例方法async

1
2
3
4
5
6
7
8
9
let mySerialDispatchQueue: DispatchQueue = DispatchQueue(label: "com.example.gcd.MySerialDispatchQueue")

mySerialDispatchQueue.async {
// 在队列中要执行的语句

DispatchQueue.main.async {
// 执行的结果可能要放到主队列中处理
}
}

除此之外,也可以同步将block追加到队列中,在Objective-C中使用dispatch_sync实现。调用了dispatch_sync的线程,必须要等它追加的任务执行结束后,才能执行后面的语句。

1
2
3
dispatch_sync(mySerialDispatchQueue, ^{
// 在队列中要执行的语句
});

在Swift中对应的是DispatchQueue的实例方法sync

1
2
3
mySerialDispatchQueue.sync {
// 在队列中要执行的语句
}

使用同步的方式追加任务时,要时刻小心是否造成死锁。比如下面这样就会产生死锁,导致程序崩溃。

1
2
3
4
dispatch_queue_main_t mainDispatchQueue = dispatch_get_main_queue();
dispatch_sync(mainDispatchQueue, ^{

NSLog(@"add task into main dispatch queue");
});

dispatch_sync在主线程中执行,主线程被阻塞等待dispatch_sync执行结束,这又导致block无法追加到主队列中,所以dispatch_sync又无法执行结束。

所以在日常开发中最好不要轻易使用同步等待处理的API,有时死锁的发生是很隐晦的,并不容易注意到,在想到用同步API的时候,再多想想还有没有其他的解决办法。

延迟执行

GCD还提供了API,可以在指定时间后将block追加到队列中,注意并不是执行,所以会有少许延迟。在Objective-C中该API是dispatch_after

1
2
3
4
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC));
dispatch_after(time, dispatch_get_main_queue(), ^{

});

第一个参数指定从何时开始计算时间,类型是dispatch_time_t,可以通过dispatch_time函数获得相对时间,也可以通过dispatch_walltime函数获得绝对时间。

在Swift中使用DispatchQueue的实例方法asyncAfter来延迟追加任务。

1
2
3
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2.0) {
// 2秒后执行
}

只执行一次

dispatch_once函数保证在应用程序执行中只执行一次指定的block。我们一般会用它来实现单例模式,以确保单例的线程安全,而且它的性能要比@synchronized要好。@synchronized每一次都要先获取锁,而dispatch_once使用一个token标识代码是否执行过。

1
2
3
4
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{

});

但是在Swift 3.0中这个函数被废弃了,但是可以使用懒加载的全局变量或静态变量,也能保证线程安全。

1
2
3
let onceTask: String = {
return "onceTask"
}()

阻碍执行

dispatch_barrier_async函数会等待在它前面已追加到队列中的任务全部执行完,然后执行由dispatch_barrier_async函数追加的任务,该任务处理结束后,再继续执行dispatch_barrier_async函数后面追加的任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
dispatch_queue_t myConcurrentDispatchQueue = dispatch_queue_create("com.example.gcd.MyConcurrentDispatchQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(myConcurrentDispatchQueue, ^{

NSLog(@"Task 0");
});
dispatch_async(myConcurrentDispatchQueue, ^{
NSLog(@"Task 1");
});
dispatch_async(myConcurrentDispatchQueue, ^{
NSLog(@"Task 2");
});
dispatch_barrier_async(myConcurrentDispatchQueue, ^{
NSLog(@"barrier");
});
dispatch_async(myConcurrentDispatchQueue, ^{
NSLog(@"Task 4");
});
dispatch_async(myConcurrentDispatchQueue, ^{
NSLog(@"Task 5");
});

由此也可以看到,对串行队列执行dispatch_barrier_async是没有多大意义的。

dispatch_barrier_async非常适合用来处理对单一资源的多读单写。对于读来说,多个线程同时读没有问题,只要保证写的时候只有一个线程在操作即可。所以使用并行队列效率会更高一些,然后在写的时候用dispatch_barrier_async控制同时只有一个线程操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- (NSString *)someString {
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}

- (void)setSomeString:(NSString *)someString {
dispatch_barrier_async(_syncQueue, ^{
_someString = someString;
});
}

在Swift 3.0中,调用async函数时,可以指定flags.barrier

1
2
3
4
5
let myConcurrentDispatchQueue = DispatchQueue(label: "com.example.gcd.MySerialDispatchQueue", attributes: .concurrent)

myConcurrentDispatchQueue.async(flags: .barrier) {

}

迭代执行

dispatch_apply函数按指定的次数将指定的block追加到指定的队列中,并等待全部的block执行完毕。block带有一个参数用来告知当前迭代的index。dispatch_apply是同步的。

dispatch_apply替代对数组等的for循环,把这些block放到并行队列中可以提高执行效率。

1
2
3
4
5
6
7
8
9
dispatch_async(myConcurrentDispatchQueue, ^{
dispatch_apply([array count], myConcurrentDispatchQueue, ^(size_t index) {
NSLog(@"%zu: %@", index, [array objectAtIndex:index]);
});

dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"done.");
});
});

同样地对串行队列执行dispatch_apply的意义不大,所以在Swift 3.0中直接就实现为并行地执行block了。使用DispatchQueue的类方法concurrentPerform

1
2
3
DispatchQueue.concurrentPerform(iterations: 10) { (index) in

}

挂起/恢复队列

dispatch_suspend函数挂起指定的队列,dispatch_resume函数恢复指定的队列。

1
2
dispatch_suspend(queue);
dispatch_resume(queue);

这两个函数不会影响已经在执行的任务,挂起后,追加到队列中的但尚未执行的处理在此之后停止执行。而恢复则使得这些处理能够继续执行。

在Swift中用起来类似。

1
2
queue.suspend()
queue.resume()

Dispatch Group

如果你想在一组并行执行的任务都完成之后再去处理其他事情,就可以使用Dispatch Group来处理。GCD提供了两种方式来实现,可以视具体情况而定用哪一种。一种是直接追加block到group中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 创建group
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, myConcurrentDispatchQueue, ^{
NSLog(@"blk0");
});
// 相继追加block
dispatch_group_async(group, myConcurrentDispatchQueue, ^{
NSLog(@"blk1");
});
dispatch_group_async(group, myConcurrentDispatchQueue, ^{
NSLog(@"blk2");
});
dispatch_group_notify(group, myConcurrentDispatchQueue, ^{
NSLog(@"done.");
});

dispatch_group_notify函数的第一个参数是要监听的group,第二个参数可以是任意queue,第三个参数是在上面提交的block都执行完后要执行的block,它会被追加到第二个参数指定的queue中。

也可以使用dispatch_group_wait函数等待前面提交到group中的block都执行完。这是个同步执行的函数,第二个参数指定要等待的group,第二个参数指定等待的超时时间,类型也是dispatch_time_t

1
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

指定为DISPATCH_TIME_FOREVER,意味着永久等待。

在Swift中对应的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
let group = DispatchGroup()
myConcurrentDispatchQueue.async(group: group) {
print("blk0")
}
myConcurrentDispatchQueue.async(group: group) {
print("blk1")
}
myConcurrentDispatchQueue.async(group: group) {
print("blk2")
}
DispatchQueue.main.async {
print("done.")
}

第二种方法用dispatch_group_enter指定提交到group中的任务开始的地方,dispatch_group_leave指定该任务结束的地方。这在不好将任务封装到block中时就派上用场了。两个函数必须成对使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
dispatch_group_enter(group);
dispatch_async(myConcurrentDispatchQueue, ^{
// 耗时任务1
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(myConcurrentDispatchQueue, ^{
// 耗时任务2
dispatch_group_leave(group);
});
dispatch_group_notify(group, myConcurrentDispatchQueue, ^{
NSLog(@"done.");
});

Swift中的对应实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let dispatchGroup = DispatchGroup()

dispatchGroup.enter()
longRunningFunction {

dispatchGroup.leave()
}

dispatchGroup.enter()
longRunningFunctionTwo {

dispatchGroup.leave()
}

dispatchGroup.notify(queue: .main) {
print("done.")
}

Dispatch Semaphore

通过前面的总结,我们知道可以通过串行队列和dispatch_barrier_async来确保同一时刻只有一个线程访问资源,我们还可以通过Dispatch Semaphore进行更细粒度的控制。Dispatch Semaphore是GCD提供的持有计数的信号,在计数为0时其他线程只能等待,当计数大于等于1时才可以继续执行。

在ObjectiveC中,通过dispatch_semaphore_signal函数将信号量加1,通过dispatch_semaphore_wait函数将信号量减1,或者在信号量为0再次执行到该函数时就会阻塞来访的线程。这样,在dispatch_semaphore_signaldispatch_semaphore_wait之间的代码片段就被保护起来了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

// 设置为0时,一开始执行到dispatch_semaphore_wait时线程就被阻塞了,直到到了你设置的等待时间。
// 设置为1时,在dispatch_semaphore_wait中又指定了DISPATCH_TIME_FOREVER,就可以达到同时只有一个线程访问被保护的代码片段。
// 设置为大于1时,就意味着允许同时有多少个线程对资源进行访问。
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);

NSMutableArray *mbArray = [[NSMutableArray alloc]init];
for (int i=0; i<100; i++) {
dispatch_async(queue, ^{
// 如果当前信号量为0就阻塞来访的线程,直到到了指定的时间。
// 如果当前信号量不为0则将其减一,但不会阻塞。
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

[mbArray addObject:[NSNumber numberWithInt:i]];
NSLog(@"%i", i);
sleep(3);

// 任务执行结束,信号量加1
dispatch_semaphore_signal(semaphore);
});
}

Swift中的实现,代码变得简洁了许多。

1
2
3
4
5
6
7
8
9
10
let semaphore = DispatchSemaphore(value: 1)
for i in 0 ..< 100 {
DispatchQueue.global().async {
// Swift中可以调用无参数的wait方法,来等待直到获得了信号量。
semaphore.wait()
print("wait: ", i)
sleep(1)
semaphore.signal()
}
}

Dispatch Source

Dispatch Source也是GCD中的成员,它是BSD系内核惯有功能kqueue的包装。使用Dispatch Source可以处理XNU内核中发生的各种事件,具体如下表所示。

名称 内容
DISPACH_SOURCE_TYPE_READ 检测到对文件系统对象的读操作
DISPACH_SOURCE_TYPE_WRITE 检测到对文件系统对象的写操作
DISPACH_SOURCE_TYPE_TIMER 监听一个定时器
DISPACH_SOURCE_TYPE_VNODE 检测到文件系统对象有变动
DISPACH_SOURCE_TYPE_SIGNAL 接收信号
DISPACH_SOURCE_TYPE_PROC 检测到与进程相关的事件
DISPACH_SOURCE_TYPE_MEMORYPRESSURE 检测到与内存压力相关的事件
DISPACH_SOURCE_TYPE_MACH_SEND MACH端口发送
DISPACH_SOURCE_TYPE_MACH_RECV MACH端口接收
DISPACH_SOURCE_TYPE_DATA_ADD 变量增加
DISPACH_SOURCE_TYPE_DATA_OR 变量OR

在开发iOS应用时,用到的比较多的可能就是用Dispatch Source来实现一个定时器了,会比NSTimer更加精准。下面来看一下Dispatch Source是怎么使用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 1.使用dispatch_source_create函数创建一个Dispatch Source
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, myDescriptor, 0, myQueue);

// 2.配置该Dispatch Source
// - 设置监听到事件时的回调
// - 如果是定时器类型的Dispatch Source,需要通过 dispatch_source_set_timer函数创建一个定时器,然后将事件处理回调与该定时器关联
dispatch_source_set_event_handler(source, ^{
size_t estimated = dispatch_source_get_data(source);

});

// 3.如果会调用dispatch_source_cancel函数取消该Dispatch Source,则再配置一下被取消时的处理回调。
dispatch_source_set_cancel_handler(source, ^{

});

// 4.调用dispatch_resume开始处理事件,因为创建完Dispatch Source不会立即就能处理事件,必须要等配置完才可以,所以要手动触发开始处理。
dispatch_resume(source);

dispatch_source_create函数需要4个参数,第1个参数指定要监听的是哪个事件,也就是上面表中列出的这些事件。第2个参数是要监视的底层系统句柄,与第1个参数指定的事件类型有关。比如监听与文件系统相关的事件,该参数就是打开文件获得的文件描述符。在没有时,可以传0。第3个参数也是要看第1个参数指定了什么事件类型,比如指定了DISPACH_SOURCE_TYPE_VNODE,但是文件的变动又具体分很多种情况,比如删除、改名字等等。我在下表中列出了其可能出现的Event Flags,可以用|指定多个类型。如果没有时可以传0。第4个参数指定对事件处理的block要提交到的队列。

事件类型 Event Flags
DISPACH_SOURCE_TYPE_VNODE DISPATCH_VNODE_LINK
DISPATCH_VNODE_WRITE
DISPATCH_VNODE_ATTRIB
DISPATCH_VNODE_DELETE
DISPATCH_VNODE_EXTEND
DISPATCH_VNODE_RENAME
DISPATCH_VNODE_REVOKE
DISPATCH_VNODE_FUNLOCK
DISPACH_SOURCE_TYPE_PROC DISPATCH_PROC_EXIT
DISPATCH_PROC_FORK
DISPATCH_PROC_SIGNAL
DISPATCH_PROC_EXEC
DISPACH_SOURCE_TYPE_MACH_SEND DISPATCH_MACH_SEND_DEAD

dispatch_source_get_data函数的返回值会根据创建Dispatch Source时指定的事件类型,确定其含义。比如你创建的是DISPACH_SOURCE_TYPE_READ类型的Dispatch Source,返回值就是预计可读取的字节数。再比如你创建的是DISPACH_SOURCE_TYPE_VNODE类型的Dispatch Source,返回值就是收到的具体是哪种变动的flag值。

关于Dispatch Source在各种事件类型时如何使用,可以看苹果官方文档中给出的例子:Dispatch Source Examples

Swift中Dispatch Source的使用变得更简单了一些,与Dispatch Queue一样,Swift将其以类的方式进行封装。因为没有了宏定义,DispatchSource类为每一种事件类型提供了对应的类方法来创建DispatchSource实例。DispatchSource遵循DispatchSourceProtocol,该协议里面定义了setCancelHandlercancelresume等等方法。

Dispatch I/O

当你要读取较大文件时,为了提高读取效率,一定记住了GCD中有一个利器可以帮助你,那就是Dispatch I/ODispatch Data的搭配使用。通过Dispatch I/O读写文件时,使用Global Dispatch Queue将一个文件按照某个大小read/write。也就是可以做到有多个线程同时操作文件,自然效率会提高。读取到的每一块数据也无需放到一个块更大的内存中进行合并,dispatch_data_t的一个相当独特的属性是它可以基于零碎的内存区域。Dispatch Data提供了dispatch_data_t dispatch_data_create_concat(dispatch_data_t data1, dispatch_data_t data2);函数,可以将两块数据组合起来,但是不会开辟新的内存空间来存放两块数据,只是retain了data1和data2。GCD还提供了提供了其他如dispatch_data_create_mapdispatch_data_create_subrange等等对dispatch_data_t的操作,具体的可以在官方文档中查到。

再说一下dispatch_io_t dispatch_io_create(dispatch_io_type_t type, dispatch_fd_t fd, dispatch_queue_t queue, void (^cleanup_handler)(int error));函数,它会返回一个只有文件描述符的创建好的通道。第一个参数type可以取值DISPATCH_IO_STREAMDISPATCH_IO_RANDOM,也就是你可以创建一个流通道或一个随机存取通道。如果你打开了硬盘上的一个文件,你可以使用它来创建一个随机存取的通道(因为这样的文件描述符是可寻址的)。如果你打开了一个套接字,你可以创建一个流通道。

接下来就是从创建好的Dispatch I/O通道读取数据了,通过void dispatch_io_read(dispatch_io_t channel, off_t offset, size_t length, dispatch_queue_t queue, dispatch_io_handler_t io_handler);函数读取,也有与之对应的写函数。

1
2
3
4
5
6
7
8
9
10
dispatch_io_read(_channel, 0, SIZE_MAX, _myQueue, ^(bool done, dispatch_data_t data, int error){
if (data != NULL) {
if (_data == NULL) {
_data = data;
} else {
_data = dispatch_data_create_concat(_data, data);
}
[self processData];
}
});

如果你只是想读取一个文件,在读取的过程中不需要做其他处理,GCD提供了一个便捷的函数dispatch_read,你只需要传文件描述符,以及在所有数据都读取完成后要执行的block。也有对应的写函数dispatch_write,你只需提供待写入的dispatch_data_t,和所有数据写入完成后要执行的block。当然这两个也是异步read/write的。

参考资料

【1】 苹果官方文档Concurrency and Application Design

【2】 ObjC中国里的这篇并行编程:API 及挑战

【3】 ObjC中国里的这篇底层并行 API

【4】 Grand Central Dispatch Tutorial for Swift 4: Part 1/2

【5】 Swift - 多线程实现方式(3) - Grand Central Dispatch(GCD)

【3】 《Objective-C 高级编程》

【4】 《Effective Objective-C 2.0》(英文版)

本文作者:意林
本文链接:http://www.shinancao.cn/2019/06/15/iOS-GCD-API
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!