Swift语法基础
NSSet、NSDictionary底层实现
Foundation框架很多都是和Core Foundation对应,例如NSSet和_CFSet相对应,NSDictionary和_CFDictionary相对应。
GCD之 Barrier,Apply,Source
Dispatch Barrier栅栏函数
简介
在并发调度队列中执行的任务的同步点
使用栅栏函数来同步调度队列中一个或多个任务的执行。在向并发调度队列添加栅栏时,该队列会延迟栅栏任务(以及栅栏之后提交的所有任务)的执行,直到所有先前提交的任务都执行完成为止。在完成先前的任务后,队列将自己执行栅栏任务。栅栏任务执行完毕之后,队列将恢复其正常执行行为。
栅栏函数
-
同步栅栏函数
dispatch_barrier_sync
:提交一个栅栏 block 以同步执行,并等待该 block 执行完;
dispatch_barrier_sync_f
:提交一个栅栏 function 以同步执行,并等待该 function 执行完。 -
异步栅栏函数
dispatch_barrier_async
:提交一个栅栏 block 以异步执行,并直接返回;
dispatch_barrier_async_f
:提交一个栅栏 function 以异步执行,并直接返回。
同步栅栏函数 dispatch_barrier_sync
1 | void dispatch_barrier_sync(dispatch_queue_t queue, dispatch_block_t block); |
dispatch_barrier_sync
同步栅栏函数会等待该函数传入的队列中的任务都执行完毕,再执行dispatch_barrier_sync
函数中的任务以及后面的任务,会阻塞当前线程。
- 使用场景:保证顺序执行,会阻塞当前线程
需求: 现有任务1、2、3、4,前两个任务执行完毕,再执行后两个任务以及主线程的代码。
解决方法:
- 使用 GCD 队列组;
- 使用
dispatch_barrier_sync
函数。
1 | dispatch_queue_t queue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_CONCURRENT); |
异步栅栏函数 dispatch_barrier_async
1 | void dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block); |
先来看一下上面的代码改为异步栅栏函数的执行效果:
1 | dispatch_queue_t queue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_CONCURRENT); |
从打印日志可以看到,改为异步栅栏函数,任务3、4仍然可以等到任务1、2以及栅栏任务都执行完毕再执行,但不会阻塞主线程,并且栅栏任务是在子线程执行。
dispatch_barrier_async
异步栅栏函数会等待该函数传入的队列中的任务都执行完毕,再执行dispatch_barrier_async
函数中的任务以及后面的任务,执行该函数会直接返回,继续往下执行代码,不会阻塞当前线程。
- 主要用于在多个异步操作完成之后,统一对非线程安全的对象进行更新;
- 适合于大规模的 I/O 操作;
- 当访问数据库或者文件的时候,更新数据的时候不能和其他更新或者读取的操作在同一时间执行,可以使用队列组不过有点复杂,可以使用dispatch_barrier_async解决;
- 保证线程安全、读写安全。
使用场景1:保证线程安全
例如: 我们要从网络上异步获取很多图片,然后将它们添加到非线程安全的对象——数组中去:异步并发。
同一时间点,可能有多个线程执行给数组添加对象的方法,所以可能会丢掉 1 到多次,我们执行 1000 次,可能数组就保存了 990 多个,还有程序出现奔溃的可能。
解决办法:
① 加锁:比较耗时,而且下载完什么时候添加进数组也不一定。我们希望所有图片都下载完,再往数组里面添加;
② 使用 GCD 队列组;
③ 使用dispatch_barrier_async函数,栅栏中的任务会等待队列中的所有任务执行完成,才会执行栅栏中的任务,保证了线程安全。
1 | dispatch_queue_t queue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_CONCURRENT); |
使用场景2:实现读写安全
dispatch_barrier_async
可以用来实现“读写安全”。我们将“写”操作放在dispatch_barrier_async
中,这样能确保在“写”操作的时候会等待前面的“读”操作完成,而后续的“读”操作也会等到“写”操作完成后才能继续执行,提高文件读写的执行效率。
1 | // 创建一个并发队列 |
1 | - (void)viewDidLoad { |
dispatch_barrier_sync
与 dispatch_barrier_async
的区别及注意点
相同点: 都会等待队列中在它们(栅栏)之前提交的任务都执行完毕,再执行它们的任务,接着执行它们后面的任务。
不同点:
dispatch_barrier_sync
:提交一个栅栏 block 以同步执行,并等待该 block 执行完;由于是同步,不会开启新的子线程,会阻塞当前线程。dispatch_barrier_async
:提交一个栅栏 block 以异步执行,并直接返回,会继续往下执行代码,不会阻塞当前线程。
注意点:
dispatch_barrier_(a)sync
函数传入的的队列必须是自己手动创建的并发队列,如果传入的是全局并发队列或者串行队列,那么这个函数是没有栅栏的效果的,效果等同于dispatch_(a)sync函数。- 只能栅栏
dispatch_barrier_(a)sync
函数中传入的queue。
Dispatch Apply多次执行
dispatch_apply
类似一个 for 循环。提交一个 block 到指定队列,并使该 block 执行指定的次数 n,等待 block 执行完 n 次才会返回。
1 | /*! |
注意点:
- 该函数会等待 block 都执行完毕才会返回,所以是同步的,会阻塞;
- 如果指定的队列是全局并发队列
dispatch_get_global_queue
,则这些 block 可以并发执行,这里需要注意可重入性; - 如果指定的队列是手动创建的并发队列,在有些情况下不会并发执行,所以建议使用全局并发队列
dispatch_get_global_queue
; - 当使用串行队列时,不会开启子线程,block 在主线程按串行执行;
- 当使用并发队列时,不一定会开启子线程,block 不一定都在子线程执行,也可能都在主线程执行,取决于任务的耗时程度。
串行队列
1 | - (void)test |
并发队列
1 | - (void)test |
- 使用场景
在某些场景下使用dispatch_apply
会对性能有很大的提升,比如你的代码需要以每个像素为基准来处理计算 image 图片。同时dispatch_apply能够避免一些线程爆炸的情况发生(创建很多线程)
1 | //危险,可能导致线程爆炸以及死锁 |
在异步中实现同步,并将任务并发执行
1 | dispatch_queue_t queue = dispatch_get_global_queue(0, 0); |
将上面代码的队列改为手动创建的并发队列,任务就不会并发执行:
1 | dispatch_queue_t queue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_CONCURRENT); |
Dispatch Source
dispatch
框架提供一套接口用于监听系统底层对象(如文件描述符、Mach 端口、信号量等),当这些对象有事件产生时会自动把事件的处理 block
函数提交到 dispatch
队列中执行,这套接口就是 Dispatch Source API
,Dispatch Source
其实就是对 kqueue
功能的封装,可以去查看 dispatch_source
的 c 源码实现(什么是 kqueue
?Google,什么是 Mach 端口? Google Again),Dispatch Source 主要处理以下几种事件:
1 | DISPATCH_SOURCE_TYPE_DATA_ADD 变量增加 |
当有事件发生时,dispatch source
自动将一个 block
放入一个 dispatch queue
执行。
GCD定时器
NSTimer
和 CADisplayLink
定时器不准时的问题,解决办法就是使用 GCD
定时器。GCD
的定时器是直接跟系统内核挂钩的,而且它不依赖于RunLoop,所以它非常的准时。
1 | dispatch_queue_t queue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_SERIAL); |
dispatch_queue_set_specific
& dispatch_get_specific
这两个 API 类似于objc_setAssociatedObject
跟objc_getAssociatedObject
,FMDB 里就用到这个来防止死锁,来看看 FMDB 的部分源码:
1 | static const void * const kDispatchQueueSpecificKey = &kDispatchQueueSpecificKey; |
当要执行数据库操作时,如果在 queue 里面的 block 执行过程中,又调用了 indatabase 方法,需要检查是不是同一个 queue,因为同一个 queue 的话会产生死锁情况
1 | - (void)inDatabase:(void (^)(FMDatabase *db))block { |
dispatch_once_t
dispatch_once_t
必须是全局或static
变量
这一条算是“老生常谈”了,但我认为还是有必要强调一次,毕竟非全局或非static
的dispatch_once_t
变量在使用时会导致非常不好排查的bug,正确的如下:
1 | //静态变量,保证只有一份实例,才能确保只执行一次 |
注意
onceToken实现单例时释放单粒 onceToken的作用域和赋值
dispatch_after
dispatch_after
是延迟提交,不是延迟运行
先看看官方文档的说明:
Enqueue a block for execution at the specified time.
Enqueue,就是入队,指的就是将一个Block在特定的延时以后,加入到指定的队列中,不是在特定的时间后立即运行!。
看看如下代面试题:
1 | //创建串行队列 |
结果如下:
1 | 2015-03-31 20:57:27.122 GCDTest[45633:1812016] Begin add block... |
参考
GCD队列优先级和队列组
GCD队列的服务质量与优先级
-
NSQualityOfServiceUserInteractive
与用户交互的任务,这些任务通常跟 UI 级别的刷新相关,比如动画,这些任务需要在一瞬间完成; -
NSQualityOfServiceUserInitiated
由用户发起的并且需要立即得到结果的任务,比如滑动scroll view
时去加载数据用于后续cell
的显示,这些任务通常跟后续的用户交互相关,在几秒或者更短的时间内完成; -
NSQualityOfServiceUtility
一些可能需要花点时间的任务,这些任务不需要马上返回结果,比如下载的任务,这些任务可能花费几秒或者几分钟的时间; -
NSQualityOfServiceBackground
这些任务对用户不可见,比如后台进行备份的操作,这些任务可能需要较长的时间,几分钟甚至几个小时; -
NSQualityOfServiceDefault
优先级介于user-initiated
和utility
,当没有 QoS 信息时默认使用,开发者不应该使用这个值来设置自己的任务。
服务质量枚举类型
1 | QOS_CLASS_USER_INTERACTIVE |
队列的优先级与服务质量的对应关系:
给队列设置QoS
dispatch_queue_attr_make_with_qos_class
dispatch_set_target_queue
GCD队列任务间依赖关系
dispatch_set_target_queue
除了能用来设置队列的 QoS 之外,还能够创建队列的层次体系。当我们想让不同队列中的任务同步的执行时,我们可以创建一个串行队列,然后将这些队列的 target 指向新创建的队列即可,比如:
1 | dispatch_queue_t targetQueue = dispatch_queue_create("target_queue", DISPATCH_QUEUE_SERIAL); |
注意点: 避免相互依赖,如将队列 A 的目标队列设置为队列 B,并将队列 B 的目标队列设置为队列 A。
Dispatch Group队列组
GCD 队列组,又称“调度组”,实现所有任务执行完成后有一个统一的回调。
例如:异步下载歌曲,等所有歌曲都下载完毕以后,转到主线程提示用户
1 | dispatch_group_t group = dispatch_group_create(); |
队列组的原理
真正实现统一回调的操作:
1 | void dispatch_group_enter(dispatch_group_t group); |
1 | dispatch_group_async(group, queue, ^{ |
1 | // 1.创建队列组 |
GCD基础
RunLoop之NSTimer
NSTimer 的创建
NSTimer
的创建通常有两种方式,一种是以 scheduledTimerWithTimeInterval
为开头的类方法 。这些方法在创建了 NSTimer
之后会将这个 NSTimer
以 NSDefaultRunLoopMode
模式放入当前线程的 RunLoop
。
1 | + ( NSTimer *) scheduledTimerWithTimeInterval:invocation:repeats: |
另一种是以 timerWithTimeInterval 为开头的类方法。这些方法创建的 NSTimer 并不能马上使用,还需要调用 RunLoop 的 addTimer:forMode: 方法将 NSTimer 放入 RunLoop,这样 NSTimer 才能正常工作。
1 | + ( NSTimer *) timerWithTimeInterval:invocation:repeats: |
Timers work in conjunction with run loops. Run loops maintain strong references to their timers, so you don’t have to maintain your own strong reference to a timer after you have added it to a run loop.
从 NSTimer
的官方文档可以得知,RunLoop
对加入其中的 NSTimer
会添加一个强引用。
RunLoop之事件循环机制、线程
RunLoop之简介、数据结构
super的本质
objc_supre与objc_msgSendSuper
objc_super 和 objc_super2
它们的区别在于第二个成员:
objc_super
:super_class //receiverClass的父类objc_super2
: current_class//receiverClass(消息接收者的class对象)
1 | // message.h(objc4) |