Swift中GCD和OC中GCD的是使用基本一致, 最大的区别就是语法, 当然还有一些细微的改动.本文主要对GCD中的一些重难点进行解释说明, 如果以前没有接触过GCD可以
目录:
DispatchQueue: 任务和队列
学习 GCD 之前,先来了解 GCD 中两个核心概念:任务和队列。
任务
任务:就是执行操作的意思,换句话说就是你在线程中执行的那段代码。在 GCD 中是放在 block 中的。执行任务有两种方式:同步执行(sync)和异步执行(async)。两者的主要区别是:是否等待队列的任务执行结束,以及是否具备开启新线程的能力。
-
同步执行(sync): 同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。 只能在当前线程中执行任务,不具备开启新线程的能力。
-
异步执行(async): 异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。 可以在新的线程中执行任务,具备开启新线程的能力。
注意:异步执行(async)虽然具有开启新线程的能力,但是并不一定开启新线程。这跟任务所指定的队列类型有关(主队列异步执行就不会开启新线程)。
队列
队列(Dispatch Queue):这里的队列指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,采用 FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务。队列的结构可参考下图:
在 GCD 中有两种队列:串行队列和并发队列。两者都符合 FIFO(先进先出)的原则。两者的主要区别是:执行顺序不同,以及开启线程数不同。
-
串行队列(Serial Dispatch Queue): 每次只有一个任务被执行。让任务一个接着一个地执行。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)
-
并发队列(Concurrent Dispatch Queue): 可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务) 注意:并发队列的并发功能只有在异步(dispatch_async)函数下才有效
-
队列(FIFO): 任务代码块的集合, 将任务放入队列后其他有系统托管
-
同步和异步: 执行队列的方式
图表:
执行方式 | 并发队列 | 串行队列 | 主队列 |
---|---|---|---|
同步(sync) | 没有开启新线程,按顺序执行任务 | 没有开启新线程,按顺序执行任务 | 死锁 |
异步(async) | 开启新线程,并行执行任务 | 只开启1条新线程,按顺序执行任务 | 没有开启新线程,在主队列按顺序执行任务 |
- 创建串行队列
1 |
|
- 创建并行队列
1 |
|
Qos: 队列优先级
并不是绝对准确, 如果将循环改为100,就会出现错乱, 案例如下
1 |
|
####Qos: 任务优先级 首先介绍DispatchWorkItem, 它将block代码块任务封装成对象的DispatchWorkItem. DispatchWorkItem是对队列中任务的封装, 和block任务快效果一样
优点:
-
DispatchWorkItem有优先级, 能限定一个队列中所有item执行的先后顺序, 同样优先级不是很靠谱, 慎用
- 能取消
- 能使用notify设置item的依赖关系
案例:
1 |
|
DispatchGroup
1 |
|
DispatchBarrier
在Swift4中, dispatchBarrier被废弃, 此功能合并到了DispatchWorkItem中的flags选项
应用: 写数据时使用,不能被读取
注意:
- 使用barrier时所有的workItem必须在同一个队列中, 不能使用globalQueue
1 |
|
DispatchSemaphore信号量
一种可用来控制访问资源的数量的标识,设定了一个信号量,在线程访问之前,加上信号量的处理,则可告知系统按照我们指定的信号量数量来执行多个线程。
其实,这有点类似锁机制了,只不过信号量都是系统帮助我们处理了,我们只需要在执行线程之前,设定一个信号量值,并且在使用时,加上信号量处理方法就行了。
主要方法:
1 |
|
semaphore.wait(timeout: DispatchTime.now() + 1)
作用:
- GCD控制并发数量的方法(NSOperation可以直接设置并发数)
- 信号量为 0: 等待, 型号量为1: 执行
应用:
1.做权限请求, 比如网络权限, 或者通讯获取权限等, 当用户同意网络权限之后信号量+1, 执行正常逻辑的代码
2.做网络请求的等待
3.控制并发数量
1 |
|
DispatchSourceTimer
注意:
-
timer一定要强引用, 否则无效
- 不收UI界面滚动的影响
- 重复的时间间隔, 最小单位为纳秒,比Timer准确
1 |
|
DispatchSource: 事件源
Dispatch Source是GCD中的一个基本类型,从字面意思可称为调度源,它的作用是当有一些特定的较底层的系统事件发生时,调度源会捕捉到这些事件,然后可以做其他的逻辑处理,调度源有多种类型,分别监听对应类型的系统事件。我们来看看它都有哪些类型:
-
Timer Dispatch Source:定时调度源。
-
Signal Dispatch Source:监听UNIX信号调度源,比如监听代表挂起指令的SIGSTOP信号。
-
Descriptor Dispatch Source:监听文件相关操作和Socket相关操作的调度源。
-
Process Dispatch Source:监听进程相关状态的调度源。
-
Mach port Dispatch Source:监听Mach相关事件的调度源。
-
Custom Dispatch Source:监听自定义事件的调度源。
用通俗一点的话说就是用GCD的函数指定一个希望监听的系统事件类型,再指定一个捕获到事件后进行逻辑处理的闭包或者函数作为回调函数,然后再指定一个该回调函数执行的Dispatch Queue即可,当监听到指定的系统事件发生时会调用回调函数,将该回调函数作为一个任务放入指定的队列中执行。也就是说当监听到系统事件后就会触发一个任务,并自动将其加入队列执行,这里与之前手动添加任务的模式不同,一旦将Diaptach Source与Dispatch Queue关联后,只要监听到系统事件,Dispatch Source就会自动将任务(回调函数)添加到关联的队列中。有些时候回调函数执行的时间较长,在这段时间内Dispatch Source又监听到多个系统事件,理论上就会形成事件积压,但好在Dispatch Source有很好的机制解决这个问题,当有多个事件积压时会根据事件类型,将它们进行关联和结合,形成一个新的事件。
监听事件类型
Dispatch Source一共可以监听六类事件,分为11个类型,我们来看看都是什么:
-
DISPATCH_SOURCE_TYPE_DATA_ADD:属于自定义事件,可以通过dispatch_source_get_data函数获取事件变量数据,在我们自定义的方法中可以调用dispatch_source_merge_data函数向Dispatch Source设置数据,下文中会有详细的演示。
- DISPATCH_SOURCE_TYPE_DATA_OR:属于自定义事件,用法同上面的类型一样。
- DISPATCH_SOURCE_TYPE_MACH_SEND:Mach端口发送事件。
- DISPATCH_SOURCE_TYPE_MACH_RECV:Mach端口接收事件。
- DISPATCH_SOURCE_TYPE_PROC:与进程相关的事件。
- DISPATCH_SOURCE_TYPE_READ:读文件事件。
- DISPATCH_SOURCE_TYPE_WRITE:写文件事件。
- DISPATCH_SOURCE_TYPE_VNODE:文件属性更改事件。
- DISPATCH_SOURCE_TYPE_SIGNAL:接收信号事件。
- DISPATCH_SOURCE_TYPE_TIMER:定时器事件。
- DISPATCH_SOURCE_TYPE_MEMORYPRESSURE:内存压力事件。
创建Dispatch Source
我们可以使用dispatch_source_create函数创建Dispatch Source,该函数有四个参数:
-
type:第一个参数用于标识Dispatch Source要监听的事件类型,共有11个类型。
- handle:第二个参数是取决于要监听的事件类型,比如如果是监听Mach端口相关的事件,那么该参数就是* mach_port_t类型的Mach端口号,如果是监听事件变量数据类型的事件那么该参数就不需要,设置为0就可以了。
- mask:第三个参数同样取决于要监听的事件类型,比如如果是监听文件属性更改的事件,那么该参数就标识文件的哪个属性,比如DISPATCH_VNODE_RENAME。
- queue:第四个参数设置回调函数所在的队列。
1 |
|
例子:
1 |
|
既然是事件处理器,那么肯定需要获取一些Dispatch Source的信息,GCD提供了三个在处理器中获取Dispatch Source相关信息的函数,比如handle、mask。而且针对不同类型的Dispatch Source,这三个函数返回数据的值和类型都会不一样,下面来看看这三个函数:
- dispatch_source_get_handle:这个函数用于获取在创建Dispatch Source时设置的第二个参数handle。
-
如果是读写文件的Dispatch Source,返回的就是描述符。
- 如果是信号类型的Dispatch Source,返回的是int类型的信号数。
- 如果是进程类型的Dispatch Source,返回的是pid_t类型的进程id。
- 如果是Mach端口类型的Dispatch Source,返回的是mach_port_t类型的Mach端口。
-
- dispatch_source_get_data:该函数用于获取Dispatch Source监听到事件的相关数据。
-
如果是读文件类型的Dispatch Source,返回的是读到文件内容的字节数。 如果是写文件类型的Dispatch Source,返回的是文件是否可写的标识符,正数表示可写,负数表示不可写。 如果是监听文件属性更改类型的Dispatch Source,返回的是监听到的有更改的文件属性,用常量表示,比如DISPATCH_VNODE_RENAME等。
-
如果是进程类型的Dispatch Source,返回监听到的进程状态,用常量表示,比如DISPATCH_PROC_EXIT等。
-
如果是Mach端口类型的Dispatch Source,返回Mach端口的状态,用常量表示,比如DISPATCH_MACH_SEND_DEAD等。
-
如果是自定义事件类型的Dispatch Source,返回使用dispatch_source_merge_data函数设置的数据。
-
- dispatch_source_get_mask:该函数用于获取在创建Dispatch Source时设置的第三个参数mask。在进程类型,文件属性更改类型,Mach端口类型的Dispatch Source下该函数返回的结果与dispatch_source_get_data一样。
注册Cancellation Handler
Cancellation Handler就是当Dispatch Source被释放时用来处理一些后续事情,比如关闭文件描述符或者释放Mach端口等。我们可以使用dispatch_source_set_cancel_handler函数或者dispatch_source_set_cancel_handler_f函数给Dispatch Source注册Cancellation Handler:
1 |
|
这里需要注意的是,如果在更改目标队列时,Dispatch Source已经监听到相关事件,并且回调函数已经在之前的队列中执行了,那么会一直在旧的队列中执行完成,不会转移到新的队列中去。
暂停恢复Dispatch Source
暂停和恢复Dispatch Source与Dispatch Queue一样,都适用dispatch_suspend和dispatch_resume函数。这里需要注意的是刚创建好的Dispatch Source是处于暂停状态的,所以使用时需要用dispatch_resume函数将其启动。
废除Dispatch Source
如果我们不再需要使用某个Dispatch Source时,可以使用dispatch_source_cancel函数废除,该函数只有一个参数,那就是目标Dispatch Source。
Dispatch Source实践
说了这么多,这一节来看看Dispatch Source到底怎么用。
用Dispatch Source监听定时器
Dispatch Source能监听的事件中有一个类型就是定时器,我们来看看如何实现:swift
1 |
|
我们来看看generateCustomEvent(dispatchSource: dispatch_source_t)方法,该方法的作用的是模拟自定义事件,首先创建一个全局并发队列,然后循环让其执行任务,在执行的任务里调用dispatch_source_merge_data函数,就可以触发监听类型为DISPATCH_SOURCE_TYPE_DATA_ADD或者DISPATCH_SOURCE_TYPE_DATA_OR的Dispatch Source。该函数有两个参数,第一个参数是目标Dispatch Source,第二个参数的类型是无符号长整型,用于向目标Dispatch Source中的对应变量追加指定的数。
我们再来看看如何监听自定义时间,首先创建类型为DISPATCH_SOURCE_TYPE_DATA_ADD的Dispatch Source,然后设置回调闭包,在闭包中使用dispatch_source_get_data获取追加的变量值,该函数只有一个参数,就是目标Dispatch Source,这里需要注意的是通过dispatch_source_get_data函数获取的变量值并不是累加值,而是每次调用dispatch_source_merge_data函数时设置的值,所以在上面的示例中用totalProcess变量累加每次获取到的值。
上面的示例可以用来模拟后台进行下载,根据下载的数据量使用dispatch_source_merge_data函数给目标Dispatch Source设置相应的变量值,然后在主线程中监听到Dispatch Source的自定义事件,通过dispatch_source_get_data函数获取到变量,用于更新显示进度条的UI。