写在前面
阅读 Apple Deleloper Run Loops 的笔记。
简介
Run Loops 是和线程息息相关的基础组件。
顾名思义,它就是一个循环,目的是:当有工作时,让线程忙碌起来;当没有工作时,让线程休眠。
Run loop 的管理不是完全自动化的,开发者仍然必须设计线程相关代码,让 run loop 在合适时间开启。
Cocoa 和 Core Foundation 都提供了 run loop 对象辅助配置和管理。
开发者不需要再创建这些对象。每个线程(包括主线程)都有一个 run loop 对象与之关联。
App 在启动时,会在主线程上设置和启动 run loop。
只有次要线程需要手动启动 run loop。
如图所示,run loop 接收2种类型数据:
- Input sources : 传递异步事件,经常是来自其他线程或不同应用的信息。
- Timer sources : 传递同步事件,定时或重复发生。
除了处理资源,run loops 也创建了关于 run loop 行为的通知。
注册一个 run loop observer 可以接受到这些通知,并且可以在线程上使用它们。
可以使用 Core Foundation 来安装 run loop observer 到线程上。
Run Loop Modes
一个 run loop mode 是 input sources 和 timer sources 的集合。
传输过程中,只有关联到 mode 的 source 才允许传递事件。
Cocoa 和 Core Foundation 都定义了默认 mode 和一些常用 mode。
开发者也可以自定义 mode。
使用 mode 可以过滤事件。
预置的 mode
Default
NSConnectionReplyMode(Cocoa)
kCFRunLoopDefaultMode(Core Foundation)
默认 mode ,在大部分操作中使用,大多数时候,应该使用这个 mode 去开启 run loop 和配置 input source。Connection
NSConnectionReplyMode(Cocoa)
Cocoa 使用这个 mode,结合 NSConnection 对象去管理回复,开发者很少需要使用它。Modal
NSModalPanelRunLoopMode(Cocoa)
Cocoa 使用这个 mode 去标记模态面板。Event tracking
NSEventTrackingRunLoopMode(Cocoa)
当用户接口追踪循环过程中,如鼠标拖拽,Cocoa 使用这个 mode 去限制即将到来的事件。Common modes
NSRunLoopCommonModes(Cocoa)
kCFRunLoopCommonModes(Core Foundation)
这是一个可以配置的组合,它已经包含了常用 mode ,开发者也可以自己添加自定义 mode 。
Input Sources
Input source 异步地传递事件给线程。
事件资源依赖于 input source 的类型,一般分为2类:
- port_based 资源:管理应用的 Mach ports。
- 自定义 input source:管理自定义事件资源。
无须关心 run loop 的类型,系统一般会实现2种类型。
这2种资源唯一的区别在于它们是如何被发信号的:
port_based 资源由内核自动发信号,而自定义资源必须手动由其他线程完成。
如果某个 input source 不是当前运行中的 mode ,它创建的任何事件都会被挂起,直到 run loop 运行到合适的 mode 。
Port_Based Sources
Cocoa 和 Core Foundation 内置创建这类 source 的支持。
比如说,开发者从不需要直接创建一个 input source ,只需要创建一个端口对象,然后使用 NSPort
的方法,添加端口到 run loop。端口对象会创建并配置好需要的 input source。
在 Core Foundation 中,你必须手动创建端口和它的 run loop 资源。
需要使用关联特定端口类型(CFMachPortRef
, CFMessagePortRef
, CFSocketRef
)的方法来创建合适的对象。
Custom Input Sources
使用 Core Foundation 中关联了 CFRunLoopSourceRef
类型的方法,来创建 custom input source,并通过很多回调方法来配置它。
为了定义 custom input source 的行为,必须定制事件传递机制。
Cocoa Perform Selector Sources
除了 port_based sources,Cocoa 定义了能在任何线程上 perform selector 的 input source。
跟 port_based sources 一样,perform selector 请求会在目标线程上排好序。
不同的是,它在执行后,需要移除自身。
Timer Sources
Timer sources 在预设的时间点,同步传递事件给线程。
Timer 不是一个实时机制,跟 input sources 一样,它与 run loop 中特定的 modes 关联,如果不在 run loop 当前的 modes 中,那么它不会被执行。
Run Loop observers
Run Loop observers 事件类型:
- The entrance to the run loop.
- When the run loop is about to process a timer.
- When the run loop is about to process an input source.
- When the run loop is about to go to sleep.
- When the run loop has woken up, but before it has processed the event that woke it up.
- The exit from the run loop.
Run Loop 事件顺序:
- Notify observers that the run loop has been entered.
- Notify observers that any ready timers are about to fire.
- Notify observers that any input sources that are not port based are about to fire.
- Fire any non-port-based input sources that are ready to fire.
- If a port-based input source is ready and waiting to fire, process the event immediately. Go to step 9.
- Notify observers that the thread is about to sleep.
- Put the thread to sleep until one of the following events occurs:
- An event arrives for a port-based input source.
- A timer fires.
- The timeout value set for the run loop expires.
- The run loop is explicitly woken up.
- Notify observers that the thread just woke up.
- Process the pending event.
- If a user-defined timer fired, process the timer event and restart the - loop. Go to step 2.
- If an input source fired, deliver the event.
- If the run loop was explicitly woken up but has not yet timed out, restart the loop. Go to step 2.
- Notify observers that the run loop has exited.
因为 timer 和 input sources 的监听通知会在这些事件实际发生前被传递。
所以,如果时间要求比较苛刻的话,可使用 sleep
和 awake-from-sleep
通知来确认时间。
何时需要使用 Run Loop?
只有创建次要线程时,才需要显式运行一个 run loop。
主线程上的 run loop 是至关重要的。
因此,app frameworks 提供了运行 main application loop 的代码,并且自动启动 run loop。
在以下情况,你需要去启动一个 run loop
- Use ports or custom input sources to communicate with other threads.
- Use timers on the thread.
- Use any of the performSelector… methods in a Cocoa application.
- Keep the thread around to perform periodic tasks.
使用 Run Loop 对象
一个 run loop 对象提供添加 input sources, timers, run-loop observers,并运行它们的主要接口,每个线程都有一个关联的 run loop 对象。Cocoa 中,它是 NSRunLoop
的实例,更底层的,它是一个指向 CFRunLoopRef opaque type 的指针。
获取 Run Loop 对象
方法:
- Cocoa 应用,使用 NSRunLoop 的
currentRunLoop
。 - 使用 CFRunLoopGetCurrent 方法。
虽然它们不是无缝连接的类型,但可以通过 NSRunLoop 里的 getCFRunLoop
方法来获得一个 CFRunLoopRef opaque type,因为它们都是引用同一 run loop,所以可以随意使用其中之一。
配置 Run Loop
运行一个 run loop 前,它必须至少添加了一个 input source 或 timer,否则无法运行它。
除了 sources,还可以安装 run loop observers,然后使用它们来判断 run loop 的运行状态。
当配置一个长时间存活的 run loop,最好得添加至少一个 input source 来接收信息。因为即使添加一个 timer 也可以运行 run loop,但一旦 timer 结束,这种方式也就失效了,这样就会造成 run loop 退出。
启动 Run Loop
只需要在次要线程中启动 Run Loop,有以下方式启动 run loop
Unconditionally
最简单方式,可以添加或移除 input sources, timers,但只能通过 kill 来停止它,而且也无法运行在“自定义 mode”。With a set time limit
比 Unconditionally 更好的方式是,运行一个带有 timeout 变量的 run loop。In a particular mode
除了 timeout 变量,可以使用特定 Mode 运行。
退出 Run Loop
方式
- Configure the run loop to run with a timeout value.
比较推荐的方式,在退出之前,run loop 处理完该处理的。 - Tell the run loop to stop.
调用CFRunLoopStop
方法也能起到使用 timeout 变量同样的效果,不同之处在于,可以在Unconditionally
启动的 run loops 上使用这项技术。
虽然移除 input sources 和 timers 也有可能会造成 run loop 退出,但这不是停止 run loop 的可靠方式。系统在需要处理事件时,会添加 input sources 到 run loop 中,因为开发者无法知道这些 input sources,所以无法移除它们,这就无法使 run loop 退出。
线程安全和 Run Loop 对象
是否安全取决于使用什么 API 来操作 run loop。
Core Foundation 中的方法一般是线程安全的,可在任何线程中调用,但最好还是在持有 run loop 的线程上调用。
Cocoa NSRunLoop 不是线程安全的,使用时,要尽量在同一持有 run loop 的线程中使用。
总结
Run Loop 是一个特殊的循环:有工作时,让线程忙碌;当没有工作时,让线程休眠。
它接收的数据类型:
- Input Sources
- Port-Based Input Sources
- Custom Input Sources
- Cocoa Perform Selector Sources
- Timer Sources
不同的 Input Sources 和 Timer Sources 的集合就是 Run Loop Modes。
除了 Sources 外,还能注册 Run Loop Observer 来接收 Run Loop 行为的通知。
只有在次级线程上,才需要使用 Run Loop 对象来管理 Run Loop。