run loop object
提供了用于将input sources
、timers
和 run-loop observers
添加到你的run loop
然后运行它的主接口。每个线程都有一个与之关联的run loop object
。在Cocoa
中,这个对象是NSRunLoop
类的一个实例。在低级应用程序(low-level application
)中,它是一个指向CFRunLoopRef
不透明类型的指针。
RunLoop 对象
获取 runloop
要获得当前线程的运行循环,可以使用以下方法之一:
- 在
Cocoa
应用程序中,使用NSRunLoop
的currentRunLoop
类方法来检索NSRunLoop
对象。 - 使用
CFRunLoopGetCurrent
函数。
NSRunLoop
类定义了一个 getCFRunLoop
方法,它返回一个可以传递给Core Foundation
例程的 CFRunLoopRef
类型。可以根据需要混合调用NSRunLoop
对象和 CFRunLoopRef
不透明类型。
配置 Run Loop
在辅助线程上运行 run loop
之前,必须向其添加至少一个 input source
或 timer
。如果 run loop
没有任何要监视的源,那么在尝试运行它时,它会立即退出。
除了安装sources
之外,还可以设置run loop observers
,并使用它们来检测 run loop
的不同执行阶段。要安装run loop observers
,请创建CFRunLoopObserverRef
不透明类型,然后使用CFRunLoopAddObserver
函数将其添加到run loop
中。run loop observers
必须使用 Core Foundation
创建,即使对于 Cocoa
应用程序也是如此。
如下是创建一个 run loop observer
并添加到 run loop
的例子:
1 | void myObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) { } |
1 | - (void)threadMain |
在为long-lived thread
(长生命周期线程)配置 run loop
时,最好至少添加一个input source
来接收消息。尽管你可以在只附加一个 timer
的情况下启动run loop
,但一旦timer
触发,它通常会失效,然后会导致run loop
退出。附加一个重复timer
可以使run loop
保持较长时间运行,但需要定期触发timer
来唤醒线程,这实际上是轮询的另一种形式。相比之下,input source
会等待事件发生,使线程保持睡眠状态直到事件发生。
启动 Run Loop
只有应用程序中的子线程才需要启动run loop
。run loop
必须至少有一个要监视的input source
或 timer
。如果没有附加,则run loop
将立即退出。
有以下几种方式来启动run loop
:
- 无条件地,(
run
函数) - 设置时间限制,(
runUntilDate:
函数) - 以特定的模式,(
runMode:beforeDate:
函数)
无条件地启动run loop
是最简单的选择,但也是最不可取的。无条件地运行run loop
会将线程放入一个永久循环中,这使你几乎无法控制run loop
本身。你可以添加和删除input source
或 timer
,但是停止运行循环的唯一方法是终止它。也无法在自定义模式下运行run loop
。
与其无条件地运行run loop
,不如使用timeout value
(超时值)运行run loop
。使用timeout value
时,run loop
将运行直到事件到达或分配的时间过期为止。如果事件到达,则将该事件分派到处理程序进行处理,然后退出run loop
。然后,你的代码可以重新启动run loop
以处理下一个事件。如果分配的时间到期了,可以简单地重新启动run loop
或使用该时间进行任何必要的内务处理。
除了timeout value
外,还可以使用特定mode
运行run loop
。模式和超时值不是互斥的,可以在启动run loop
时同时使用。模式限制向run loop
传递事件的源(sources
)的类型。
如下例子运行 runloop:
1 | - (void)skeletonThreadMain |
可以递归运行run loop
。换句话说,可以从input source
或timer
的处理程序例程中调用CFRunLoopRun
,CFRunLoopRunInMode
或任何NSRunLoop
方法来启动run loop
。这样做时,可以使用任何要运行嵌套run loop
的模式,包括外部run loop
使用的模式。
退出 Run Loop
在处理事件之前,有两种方法可以使run loop
退出:
- 配置
run loop
以使用timeout value
运行。 - 告诉
run loop
停止。
如果你可以管理timeout value
的话,使用timeout value
当然是首选的。指定timeout value
可以让run loop
在退出之前完成其所有正常处理,包括向run loop observers
发送通知。
使用 CFRunLoopStop
函数显式地停止run loop
会产生类似于超时的结果。run loop
发送所有剩余的运行循环通知,然后退出。区别在于,可以在无条件启动的run loop
上使用此技术。
虽然删除run loop
的 input sources
和 timers
也可能导致 run loop
退出,但这不是停止 run loop
的可靠方法。一些系统例程将 input sources
添加到 run loop
以处理所需的事件。因为你的代码可能不知道这些input sources
,所以它将无法删除它们,这将无法使 run loop
退出。
线程安全和运行循环对象
线程安全性取决于你用于操作run loop
的 API。Core Foundation
中的函数通常是线程安全的,可以从任何线程调用。但是,如果执行的操作更改了 run loop
的配置,则仍然最好从拥有 run loop
的线程进行更改。
Cocoa
的NSRunLoop
类并不像它的Core Foundation
对应的类那样本质上是线程安全的。如果你使用NSRunLoop
类来修改你的run loop
,则只能从拥有那个run loop
的同一个线程中进行修改。将 input sources
或 timers
添加到属于不同线程的run loop
中可能会导致代码崩溃或异常行为。
配置 Run Loop Sources
以下各节展示了如何在 Cocoa
和 Core Foundation
中设置不同类型的 input sources
的示例。
定义 Custom Input Source
创建Custom Input Source
(自定义输入源)需要定义以下内容:
- 希望
input source
处理的信息。 - 一个调度程序例程(
scheduler routine
),让感兴趣的clients
知道如何联系你的input source
。 - 用于执行任何
clients
发送的请求的handler routine
(处理程序例程)。 - 取消例程(
cancellation routine
)使input source
无效。
因为你创建了一个 Custom Input Source
来处理自定义信息,所以实际的配置设计得很灵活。调度程序(scheduler
)、处理程序(handler
)和取消例程(cancellation routines
)是 custom input source
几乎总是需要的key routines
(关键例程)。但是,input source
的其余大部分行为发生在这些处理程序例程之外。例如,由你定义将数据传递到input source
和将input source
的存在与其他线程通信的机制。
如下图显示了 Custom Input Source
的配置示例。在此示例中,应用程序的主线程维护对input source
、该input source
的自定义命令缓冲区以及安装该input source
的run loop
的引用。
当主线程有需要要移交给worker thread
的任务时,它会向command buffer
(命令缓冲区)发布命令,以及worker thread
启动任务所需的任何信息。(由于主线程和worker thread
的input source
都可以访问command buffer
,因此必须同步访问。)发布命令后,主线程会向input source
发出信号,并唤醒worker thread
的run loop
。收到唤醒命令后,run loop
将调用input source
的handler
,该handler
将处理在command buffer
中找到的命令。
可以从官方文档上找到上图中解释的custom input source
的实现,以及你需要实现的关键代码。
配置 Timer Sources
要创建timer source
,你要做的就是创建一个timer object
并将其安排在run loop
上。在Cocoa
中,使用NSTimer
类创建新的timer objects
,在Core Foundation
中,使用CFRunLoopTimerRef
不透明类型。在内部,NSTimer
类只是Core Foundation
的扩展,它提供了一些便利功能,例如使用同一方法创建和安排计时器的功能。
在Cocoa
中,可以使用以下两种方法之一同时创建和安排计时器:
1 | scheduledTimerWithTimeInterval:target:selector:userInfo:repeats: |
上面方法创建timer
,并以默认模式(NSDefaultRunLoopMode
)将其添加到当前线程的run loop
中。如果需要,可以手动安排timer
,方法是创建NSTimer
对象,然后使用NSRunLoop
的addTimer:forMode:
方法将其添加到run loop
中。这两种技术都做着基本相同的事情,但是可以为你提供对timer
配置的不同级别的控制。例如,如果你创建timer
并将其手动添加到run loop
中,则可以使用默认模式以外的其他模式来执行此操作。
如下使用 NSTimer 创建和调度计时器:
1 | NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop]; |
配置 Port-Based Input Source
Cocoa
和 Core Foundation
都提供了port-based objects
,用于在线程之间或进程之间进行通信。以下各节说明如何使用几种不同类型的port
来设置port
通信。
配置 NSMachPort
要与 NSMachPort
对象建立本地连接,请创建port object
并将其添加到主线程的run loop
中。启动辅助线程时,将同一对象传递给线程的entry-point function
(入口点函数)。辅助线程可以使用相同的对象将消息发送回主线程。
配置 NSMessagePort
要与 NSMessagePort
对象建立本地连接,不能简单地在线程之间传递端口对象。远程消息端口必须按名称获取。要在 Cocoa
中实现此功能,需要使用特定名称注册你的本地端口,然后将该名称传递给远程线程,以便它可以获取适当的端口对象以进行通信。