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 中实现此功能,需要使用特定名称注册你的本地端口,然后将该名称传递给远程线程,以便它可以获取适当的端口对象以进行通信。