事件驱动模型
大部分的 GUI 程序都使用了事件驱动模型,有些服务器(nginx, node.js)也使用了事件驱动模型。相比于轮询等其他方式,其优点在于极大的提高了 CPU 使用率,在没有事件的时候,能够让出 CPU 时间片,来事件时也可以快速的得到响应。
Cocoa 中的 RunLoop
我们常用到的是 NSRunLoop ,其位于 Foundation 框架中。Foundation 框架实际上是
Core Foundation 的部分导出。而 Core Foundation 的源码是开源的。NSRunLoop 实际上是 CFRunLoop 的高层抽象。CFRunLoop 源码可以从这里 下载到。RunLoop 在 Cocoa 中的应用相当广泛。
NSTimer,UIEvent,NSObject (NSDelayedPerforming),NSObject (NSThreadPerformAdditions),NSURLConnection 是通过 Source 的机制把事件加入到 RunLoop 中。
Autorelease,CADisplayLink,CATransition,CAAnimation 则是利用 observer 的机制,在 RunLoop 的循环周期中执行各自的操作。
更加底层的 GCD,mach kernel 也与 RunLoop 存在协作关系。
使用 Xcode 调试的时候我们经常可以看到 RunLoop 相关的堆栈信息:

runloop断点
图中可以看到四处关于 RunLoop 的堆栈信息,最上面一条
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ 则是 RunLoop 中的 observer 的 call out 调用。其它的调用还包括:图中还可以看出 CATransition 利用了 RunLoop 的 observer 做视图的相关操作。
RunLoop 机制

RunLoop结构
这一张图我们可以看出 RunLoop 的组织结构关系。
下面针对各个部分进行了说明
CFRunLoop
- CFRunLoop 里面包含了线程,若干个 mode。
- CFRunLoop 和线程是一一对应的。
- _blocks_head 是 perform block 加入到里面的
CFRunLoopMode
- runloop mode 里面包含了source0、source1、timer、observer 各种事件,以及 port 集合。其负责管理这些事件。
CFRunLoopMode 包含下面几种 mode
mode | name | description |
Default | NSDefaultRunLoopMode (Cocoa) kCFRunLoopDefaultMode (Core Foundation) | 用的最多的模式,大多数情况下应该使用该模式开始 RunLoop并配置 input source |
Connection | NSConnectionReplyMode (Cocoa) | Cocoa用这个模式结合 NSConnection 对象监测回应,我们应该很少使用这种模式 |
Modal | NSModalPanelRunLoopMode (Cocoa) | Cocoa用此模式来标识用于模态面板的事件 |
Event tracking | NSEventTrackingRunLoopMode (Cocoa) | Cocoa使用此模式在鼠标拖动loop和其它用户界面跟踪 loop期间限制传入事件 |
Common modes | NSRunLoopCommonModes (Cocoa) kCFRunLoopCommonModes (Core Foundation) | 这是一组可配置的常用模式。将输入源与些模式相关联会与组中的每个模式相关联。Cocoa applications 里面此集包括Default、Modal和Event tracking。Core Foundation只包括默认模式,你可以自己把自定义mode用CFRunLoopAddCommonMode函数加入到集合中. |
RunLoopSource
RunLoopSource 分为 source0 和 source1。
- source0 是非基于 port 的事件,主要是 APP 内部事件,如点击事件,触摸事件等。
- source1 是基于Port的,通过内核和其他线程通信,接收,分发系统事件。
- __CFRunLoopSource 里面包含一个 _runLoops,也就意味着一个 __CFRunLoopSource 可以被添加到多个 runloop mode 中去。
RunLoopObserver
CFRunLoopObserver 是观察者,可以观察RunLoop的各种状态,并抛出回调。可以监听得状态如下:
RunLoopTimer
- CFRunLoopTimer 是定时器,可以在设定的时间点抛出回调
- CFRunLoopTimer和NSTimer是toll-free bridged的,可以相互转换。
CFRunLoopGet
- RunLoop 和线程是一一对应的
- 不允许直接创建RunLoop
- 线程刚创建时并没有 RunLoop, 如果你不主动获取, 那它一直都不会有
- 你只能在一个线程的内部获取其 RunLoop(main thread 除外)
- RunLoop 会在线程销毁时销毁
- RunLoop 和线程的一一对应关系保存一个全局 Dictionary 里 __CFRunLoops
- 即使主线程,不主动创建也不会有 RunLoop,只是 application 会在主线程默认创建 RunLoop
- 如果创建子线程 RunLoop,主线程会默认加上一个 RunLoop
CFRunLoopRun

RunLoopRun
执行过程大致描述如下:
- 通知 observers 即将进入 run loop
- 通知 observes 即将开始处理 timer source
- 通知 observes 即将开始处理 source0 事件
- 执行 source0 事件
- 如果处于主线程有 dispatchPort 消息,跳转到第9步
- 通知 observes 线程即将进入休眠
- 内循环阻塞等待接收系统消息,包括:
- 收到内核发送过来的消息 (source1消息)
- 定时器事件需要执行
- run loop 的超时时间到了
- 手动唤醒 run loop
- 通知 observes 线程被唤醒
- 处理通过端口收到的消息:
- 如果自定义的 timer 被 fire,那么执行该 timer 事件并重新开始循环,完成后跳转到第2步
- 如果 input source 被 fire,则处理该事件
- 如果 run loop 被手动唤醒,并且没有超时,完成后跳转到第2步
- 通知 observes run loop 已经退出
注意:
- CFRunLoopDoBlocks 是执行 perform block 中的 block
- 绿色的是RunLoopRun()
- 第一次循环 CFRunLoopServiceMachPort 是不走的
- handle_msg 处理 timer 事件,处理 main queue block 事件,处理 source1 事件
- 中间的红色
CFRunLoopServiceMachPort是监听 GCD 的端口事件,只监听一个端口,左下角的CFRunLoopServiceMachPort是坚挺 source1,timer 的,是一个 MutableSet
RunLoop 与 GCD
RunLoop 与 GCD 是互相协作的关系,RunLoop 的最开始部分使用了 GCD 的 timer 做超时的回调;通过 GCD 调用带有 RunLoop 的线程的 block,会通过
dispatch port CFRunLoopServiceMachPort 把事件发送到该线程的 RunLoop 里面。比如:
主线程存在 runloop,那么 GCD 会通过
dispatch port CFRunLoopServiceMachPort,把事件发送给 RunLoop,RunLoop 接收到时间之后,会执行这个 block。NSTimer 与 GCD Timer
NSTimer 是通过 RunLoop 的 RunLoopTimer 把时间加入到 RunLoopMode 里面。官方文档里面也有说 CFRunLoopTimer 和 NSTimer 是可以互相转换的。由于 NSTimer 的这种机制,因此 NSTimer 的执行必须依赖于 RunLoop,如果没有 RunLoop,NSTimer 是不会执行的。
GCD 则不同,GCD 的线程管理是通过系统来直接管理的。GCD Timer 是通过 dispatch port 给 RunLoop 发送消息,来使 RunLoop 执行相应的 block,如果所在线程没有 RunLoop,那么 GCD 会临时创建一个线程去执行 block,执行完之后再销毁掉,因此 GCD 的 Timer 是不依赖 RunLoop 的。
至于这两个 Timer 的准确性问题,如果不再 RunLoop 的线程里面执行,那么只能使用 GCD Timer,由于 GCD Timer 是基于 MKTimer(mach kernel timer),已经很底层了,因此是很准确的。
如果在 RunLoop 的线程里面执行,由于 GCD Timer 和 NSTimer 都是通过 port 发送消息的机制来触发 RunLoop 的,因此准确性差别应该不是很大。如果线程 RunLoop 阻塞了,不管是 GCD Timer 还是 NSTimer 都会存在延迟问题。
应用
- 异步的回调如果存在延时操作,那么就要放到有 RunLoop 的线程里面,否则回调没有着陆点无法执行
- NSTimer 必须得在有 RunLoop 的线程里面才能执行,另外,使用 NSTimer 的时候会出现滑动 TableView,Timer 停止的问题,是由于 RunLoopMode 切换的问题,只要把 NSTimer 加到 common mode 就好了。
- 滚动过程中延迟加载,可以利用滚动时 RunLoopMode 切换到 NSEventTrackingRunLoopMode 模式下这个机制,在 Default mode 下添加加载图片的方法,在滚动时就不会触发。
- 崩溃后处理 DSSignalHandlerDemo
演示代码
代码中演示了 RunLoop 的不同事件机制,也演示了 GCD Timer 和 NStimer 的区别。
Demo: RunLoopTest
源码注释: source
PPT: 深入理解 RunLoop