原文链接

RunLoop 是线程相关的基础结构的一部分。RunLoop 是一个事件处理循环,用于调度工作和协调传入事件的接收。RunLoop 的目的是在有工作要做时让线程保持忙碌,在没工作时让线程休眠。

RunLoop 的管理并不是完全自动的。你仍然必须设计线程的代码,以便在适当的时候启动 RunLoop 并相应传入的事件。Cocoa 和 CoreFoundation 都提供 RunLoop 对象来帮助你配置和管理线程的 RunLoop。你的应用不需要显式地创建这些对象;每个线程,包括应用的主线程,都有一个关联的 RunLoop 对象。但是,只有辅助线程需要显式地运行他们的 RunLoop。作为应用启动过程的一部分,应用框架在主线程上自动设置和运行 RunLoop。

下面几节提供关于 RunLoop 以及如何为应用配置它们的更多信息。有关 RunLoop 对象的其他信息,参见 《NSRunLoop Class Reference》《CFRunLoop Reference》

RunLoop 剖析

RunLoop 如其名。它是线程进入并用于运行事件处理程序以响应传入事件的循环。你的代码提供了用于实现运行循环和实际循环部分的控制语句——换句话说,你的代码提供了驱动 RunLoop 的 while 或 for 循环。在循环中,使用 RunLoop 对象「运行」事件处理代码,该代码接受事件并调用已安装的处理程序。

RunLoop 从两种不同类型的源接收时间。输入源传递异步事件,通常是来自另一个线程或不同应用程序的消息。定时器源交付同步事件,发生在预订的时间或重复的间隔。这两种类型的源都使用特定于应用的处理程序例程在事件到达时处理它。

Figure 3-1 显示了 RunLoop 和各种源的概念结构。输入源将异步事件交付给相应的处理程序,并因 runUntilDate: 方法(在线程关联的 NSRunLoop 对象上调用)退出。定时器源将事件交付给其处理程序例程,但不会导致 RunLoop 退出。

Figure 3-1 Structure of a run loop and its sources

除处理输入源之外,RunLoop 还生成关于 RunLoop 行为的通知。注册的 RunLoop 观察者可以接收这些通知,并使用它们在线程上执行额外的处理。你可以使用 CoreFoundation 在线程上安装 RunLoop 观察器。

下面几节提供关于 RunLoop 运行循环的组件及其操作模式的更多信息。它们还描述处理事件期间的不同时期产生的通知。

RunLoop 模式

RunLoop 模式是要监视的输入源和定时器的集合,以及要通知的 RunLoop 观察者的集合。每次 RunLoop 运行时,你都(显式或隐式地)指定要运行的特定模式。在 RunLoop 传递过程中,只监视与该模式关联的源,并允许交付他们的事件。(类似地,只有与模式相关的观察者才会被告知 RunLoop 的进度。)与其他模式关联的源将保留每个新事件,直到后续以适当的模式通过循环。

在代码中,通过名称标识模式。Cocoa 和 CoreFoundation 都定义了默认模式和几种常用模式,以及在代码中指定这些模式的字符串。你可以通过简单地为模式名称指定一个自定义字符串来定义自定义模式。尽管为自定义模式分配的名称是任意的,但这些模式的内容不是任意的。你必须确保一个或多个输入源、计数器和 RunLoop 观察者添加到你创建的任何模式中,以使其有用。

在通过运行 RunLoop 的特定通道期间,可以使用模式过滤掉不需要的源中的事件。大多数情况下,你希望以系统定义的「default」模式运行 RunLoop。然而模态面板可能以「modal」模式运行。在这种模式下,只有与模态面板相关的源才会将事件交付给线程。对于辅助线程,可以使用自定义模式来防止低优先级源在事件关键型操作期间交付事件。

Note: 模式的区别是基于事件的来源,而不是事件的类型。例如,你不会使用模式只匹配鼠标按下事件或键盘事件。你可以使用模式来侦听不同的端口集、临时挂起定时器,更改源和当前监听的 RunLoop 观察者。

Table 3-1 列出了 Cocoa 和 CoreFoundation 定义的标准模式,以及使用该模式时的描述。名称列列出了用于在代码中指定模式的实际常量。

Table 3-1 Predefined run loop modes

模式 名称 描述
Default NSDefaultRunLoopMode (Cocoa) kCFRunLoopDefaultMode (Core Foundation) 默认模式是用于大多数操作的模式。大多数情况下,你应该使用此模式启动 RunLoop 并配置输入源
Connection NSConnectionReplyMode (Cocoa) Cocoa 将这种模式与 NSConnection 对象结合使用来监控回复。你应该很少需要使用这种模式
Modal NSModalPanelRunLoopMode (Cocoa) Cocoa 使用此模式来标识用于模态面板的事件
Event tracking NSEventTrackingRunLoopMode (Cocoa) Cocoa 使用这种模式在鼠标拖动循环和其他类型用户界面跟踪循环期间限制传入的事件
Common modes NSRunLoopCommonModes (Cocoa) kCFRunLoopCommonModes (Core Foundation) 这是一组可配置的常用模式。将输入源与此模式关联还将其与组中的各个模式关联。对于 Cocoa 应用,默认情况下,该设置包括了 Default、Modal、EventTracking 模式。CoreFoundation 最初最包含默认模式。你可以使用 CFRunLoopAddCommonMode 函数向集合添加自定义模式

Input Sources - 输入源

输入源异步地将事件交付给线程。事件的源取决于输入源的类型,输入源通常是两类中的一种。基于端口的输入源监视应用程序的 Mach 端口。自定义输入源监视事件的自定义源。就 RunLoop 而言,输入源是基于端口的还是自定义的并不重要。系统通常实现两种类型的输入源,你可以按原样使用它们。这两个源之间的唯一区别是他们的信号是如何发出的。基于端口的源由内核自动发出信号,而自定义源必须由另一个线程手动发出信号。

创建输入源时,将其分配给 RunLoop 的一个或多个模式。模式会影响在任何给定时刻监视哪些输入源。大多数情况下,以默认模式运行 RunLoop,但也可以指定自定义模式。如果输入源不在当前监视模式中,则它生成的任何事件都将保留到运行循环以正确的模式运行为止。

下面几节描述一些输入源。

基于端口的输入源

Cocoa 和 CoreFoundation 为使用与端口相关的对象和函数创建基于端口的输入源提供了内置支持。例如,在 Cocoa 中,根本不需要直接创建输入源。你只需创建一个端口对象,并使用 NSPort 方法将该端口添加到 RunLoop 中。端口对象为你处理所需输入源的创建和配置。

在 CoreFoundation 中,你必须手动创建端口及 RunLoop 源。在这两种情况下,你都可以使用端口不透明类型(CFMachPortRef, CFMessagePortRef, 或 CFSocketRef) 关联的函数来创建适当的对象。

有关如何设置和配置基于自定义端口的源的示例。参见 《Configuring a Port-Based Input Source》

自定义输入源

要创建自定义输入源,你必须使用 CoreFoundation 中 CFRunLoopSourceRef 不透明类型的关联函数。使用几个回调函数配置自定义输入源。CoreFoundation 在不同位置调用这些函数来配置源,处理任何传入事件,并在源从 RunLoop 中移除时移除它。

除了定义事件到达时自定义源的行为之外,还必须定义事件交付机制。源的这一部分运行在单独的线程上,负责为输入源提供数据,并在数据准备好进行处理时发出信号。事件交付机制由你决定,但不必过于复杂。

有关如何创建自定义源的示例,参见 《Defining a Custom InputSource》。有关自定义源的参考信息,参见 《CFRunLoopSource Reference》

Cocoa 执行选择器源

除基于端口的源之外,Cocoa 还定义了一个自定义输入源,允许你在任何线程上执行选择器。与基于端口的源一样,执行选择器请求在目标线程上序列化,从而减轻了在一个线程上运行多个方法可能出现的许多同步问题。与基于端口的源不同,执行选择器源会在执行选择器之后从 RunLoop 中移除自己。

Note: 在 OS X v10.5 之前,执行选择器源主要用于向主线程发送消息,但是 OS X v10.5 及之后的 iOS 中,你可以使用它们想任何线程发送消息。

在另一个线程执行选择器时,目标线程必须有一个活跃的 RunLoop。对于创建的线程,这意味着等待代码显式地启动 RunLoop。但是,由于主线程启动自己的 RunLoop,所以一旦应用调用程序委托方法 applicationDidFinishLaunching: 时,你就可以开始在线程上发起调用。 RunLoop 每次通过循环处理所有排队的执行选择器调用,而不是在每次循环迭代期间处理一个。

Table 3-2 列出了 NSObject 上定义的方法,这些方法可用于在其他线程上执行选择器。因为这些方法是在 NSObject 上声明的,所以可以在访问 Objective-C 对象的任何线程中使用它们,包括 POSIX 线程。这些方法实际上并不创建执行选择器的新线程。

Table 3-2 Performing selectors on other threads

方法 描述
performSelectorOnMainThread:withObject:waitUntilDone: performSelectorOnMainThread:withObject:waitUntilDone:modes: 在应用主线程的下一个 RunLoop 周期执行指定选择器。这些方法提供了在执行选择器之前阻塞当前线程的选项
performSelector:onThread:withObject:waitUntilDone: performSelector:onThread:withObject:waitUntilDone:modes: 在具有 NSThread 对象的任何线程上执行指定的选择器。这些方法提供了在执行选择器之前阻塞当前线程的选项
performSelector:withObject:afterDelay: performSelector:withObject:afterDelay:inModes: 在当前线程下一个 RunLoop 周期和一个可选的延迟周期之后执行选择器。因为它将等待到下一个 RunLoop 周期,所以这些方法提供了来自当前执行代码的自动小延迟。多个排队中的选择器按照他们排队的顺序一个接一个执行
cancelPreviousPerformRequestsWithTarget: cancelPreviousPerformRequestsWithTarget:selector:object: 允许你取消 performSelector:withObject:afterDelay:performSelector:withObject:afterDelay:inModes: 方法发送到当前线程的消息

关于这些方法的详细信息,参见 《NSObject Class Reference》

Timer Sources - 定时器源

定时器源在将来一个预先设置的时间同步向线程交付时间。定时器是线程通知自己做某事的一种方式。例如,搜索字段可以在用户连续击键间隔一定时间后使用定时器使用自动搜索。使用此延迟时间使用户有机会在搜索之前键入尽可能多的所需搜索字符串。

尽管定时器是基于时间的通知,但它并不是实时机制。与输入源一样,定时器和 RunLoop 的特定模式相关联。如果定时器不处于 RunLoop 当前监视的模式,则在你以定时器支持的模式运行 RunLoop 之前,定时器不会触发。类似地,如果定时器在执行处理程序例程的过程中触发,则定时器将等待下一次通过 RunLoop 调用其处理程序例程。如果 RunLoop 根本没有运行,则定时器永远不会触发。

可以将定时器配置为一次事件或多次事件。重复定时器根据计划的触发时间(而不是实际的触发时间)自动重新调度自身。例如,如果一个定时器计划在某个特定时间触发,并且再次之后每隔 5s 触发一次,计划触发的时间总是落在原来 5s 的时间间隔上,即使实际的触发时间被延迟。如果触发时间延迟太多,以至于错过了一个或多个预定的触发时间,则定时器在错过的时间段只触发一次。在对错过的时间段进行触发后,定时器将被重新调度到下一次预订的触发时间。

关于配置定时器源的更多信息,参见 《Configuring Timer Sources》。相关信息参见 《NSTimer Class Reference》《CFRunLoopTimer Reference》

RunLoop 观察者

与源不同,源在适当的异步或同步事件发生时触发,RunLoop 观察者在 RunLoop 本身执行期间在等待位置触发。你可以使用 RunLoop 观察者来准备线程处理给定的事件,或者在线程进入休眠之前准备线程。你可以将 RunLoop 观察者与 RunLoop 中以下事件关联起来:

  • RunLoop 进入
  • RunLoop 即将处理定时器时
  • RunLoop 将要处理输入源时
  • RunLoop 即将进入休眠状态时
  • RunLoop 已被唤醒,但在处理唤醒它的事件之前
  • 退出 RunLoop

你可以使用 CoreFoundation 向应用添加 RunLoop 观察者。要创建 RunLoop 观察者,你需要创建一个 CFRunLoopObserverRef 不透明类型的新示例。该类型跟踪自定义回调函数机器感兴趣的活动。

与定时器类似,RunLoop 观察者可以使用一次或多次。一个一次性的观察者在它被触发后就将自己从 RunLoop 中移除,而一个重复的观察者触发后仍然还在。你可以在创建观察者时指定它是一次性运行还是重复运行。

有关如何创建 RunLoop 观察者的示例,参见 《Configuring the Run Loop》。其他参考信息,参见 《CFRunLoopObserver Reference》

RunLoop 事件序列

每次运行它时,线程 RunLoop 挂起的事件并为每个附加观察者生成通知。它这样做的顺序非常具体,如下所示:

  1. 通知观察者 RunLoop 已经进入
  2. 通知观察者准备好的定时器即将触发
  3. 通知观察者任何非基于端口的输入源即将触发
  4. 启动任何准备启动的非基于端口的输入源
  5. 如果基于端口的输入源准备好并等待启动,则立即处理该事件,转到步骤 9
  6. 通知观察者线程即将休眠
  7. 线程休眠,直到发生以下事件之一
    • 基于端口的输入源事件到达
    • 定时器触发
    • RunLoop 被显式唤醒
  8. 通知观察者线程刚刚唤醒
  9. 处理挂起的事件
    • 如果用户定义的定时器触发,处理定时器事件并重新循环。转到步骤 2
    • 如果触发输入源,则交付事件
    • 如果 RunLoop 被显式唤醒,但尚未超时,则重新启动循环。转到步骤 2
  10. 通知观察者 RunLoop 循环已经退出。

由于定时器和输入源的观察者通知是这些事件实际发生之前交付的,因此在通知的时间和实际事件的时间之间可能存在差距。如果这些事件之间的时间间隔非常关键,你可以使用 sleep 和 awake-from-sleep 通知来帮助你关联实际事件之间的时间间隔。

如果定时器和其他周期性事件是运行 RunLoop 时交付的,绕过该循环会破坏这些事件的交付。每当你通过输入源循环并重复从应用请求事件来实现鼠标跟踪例程时,就会出现这种行为的典型示例。因为你的代码直接捕获事件,而不是让应用正常分发这些事件,所以鼠标跟踪例程退出并将控制权返回给应用之前,活动计时器将无法触发。

可以使用 RunLoop 对象显式唤醒 RunLoop。其他时间也可能导致 RunLoop 被唤醒。例如,添加另一个非基于端口的输入源将唤醒 RunLoop,以便可以立即处理输入源,而不是等待其他事件发生。

何时使用 RunLoop

唯一需要显式运行 RunLoop 的时间是应用创建辅助线程的时候。应用主线程的 RunLoop 是基础结构的关键部分。应用框架提供了运行住应用循环的代码,并自动启动该循环。iOS 中的 UIApplication(或 OS X 中的 NSApplication)的运行方法作为正常启动序列的一部分启动应用的主循环。如果你使用 Xcode 模板项目来创建你的应用,你永远不应该显式地调用这些例程。

对于辅助线程,你需要决定是否需要 RunLoop,如果需要,则自行配置并启动它。在所有情况下,你不需要启动线程的 RunLoop。例如,如果你使用一个线程来执行一些长时间运行和预先确定的任务,那么你可能可以避免启动 RunLoop。运行循环适用于希望与线程进行更多交互的情况。例如,如果你计划执行以下操作,则需要启动一个 RunLoop:

  • 使用端口或自定义输入源与其他线程通信
  • 在线程上使用定时器
  • 在 Cocoa 应用中使用 performSelector… 方法
  • 保持线程运行以执行周期性任务

如果选择使用 RunLoop,啧配置和设置非常简单。但是,和所有线程编程一样,你应该有一个在适当情况下退出辅助线程的计划。通过让线程退出来干净地结束它总比强制它终止要好。有关如果配置和退出 RunLoop 的信息在 《Using Run Loop Objects》 中描述。

使用 RunLoop 对象

RunLoop 对象提供主接口,用于向其加入输入源、定时器和循环观察者然后运行。每个线程都有一个与之关联的 RunLoop 对象。在 Cocoa 中,这个对象是一个 NSRunLoop 类的示例。在一个低级应用中,它指向 CFRunLoopRef 不透明类型的指针。

获取一个 RunLoop 对象

要获得当前线程的 RunLoop,可使用以下方法之一:

  • 在 Cocoa 应用中,使用 NSRunLoop 的 currentRunLoop 类方法来检索 NSRunLoop 对象
  • 使用 CFRunLoopGetCurrent 函数

虽然它们都不是 toll-free 桥接类型,但你可以在需要时从 NSRunLoop 对象获得 CFRunLoopRef 不透明类型。NSRunLoop 定义了一个 getCFRunLoop 方法,该方法返回一个 CFRunLoopRef 类型,你可以将其传递给 CoreFoundation 例程。因为这两个对象都引用相同的 RunLoop,所以你可以根据需要混合对 NSRunLoop 对象和 CFRunLoopRef 不透明类型的调用。

配置 RunLoop

在辅助线程中运行 RunLoop 之前,必须至少向其添加一个输入源或定时器。如果一个 RunLoop 没有任何要监视的源,那么当你试图运行它时,它将立即退出。有关如何向 RunLoop 中添加源的示例,参见 《Configuring Run Loop Sources》

除安装源外,你还可以安装 RunLoop 观察者,并使用它们来检测 RunLoop 的不同执行阶段。要安装 RunLoop 观察者,你需要创建一个 CFRunLoopObserverRef 不透明类型,并使用 CFRunLoopAddObserver 函数将其添加到你的 RunLoop 中。RunLoop 观察者必须使用 CoreFoundation 创建,即使对于 Cocoa 应用也是如此。

Listing 3-1 显示了 RunLoop 观察者添加到 RunLoop 的线程的主例程。这个示例的目的是想你展示如何创建一个 RunLoop 观察者,因此代码只是设置一个 RunLoop 观察则来监视所有的 RunLoop 活动。基本处理程序例程(未显示)只是在处理定时器请求时记录 RunLoop 活动。

Listing 3-1 Creating a run loop observer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
- (void)threadMain
{
// The application uses garbage collection, so no autorelease pool is needed.
NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];

// Create a run loop observer and attach it to the run loop.
CFRunLoopObserverContext context = {0, self, NULL, NULL, NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities, YES, 0, &myRunLoopObserver, &context);

if (observer)
{
CFRunLoopRef cfLoop = [myRunLoop getCFRunLoop];
CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
}

// Create and schedule the timer.
[NSTimer scheduledTimerWithTimeInterval:0.1 target:self
selector:@selector(doFireTimer:) userInfo:nil repeats:YES];

NSInteger loopCount = 10;
do
{
// Run the run loop 10 times to let the timer fire.
[myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
loopCount--;
}
while (loopCount);
}

在为持久线程配置 RunLoop 时,最好至少添加一个输入源来接收消息。尽管你可以在仅添加定时器的情况下进入 RunLoop,但是一旦定时器触发,它通常会失效,这将导致 RunLoop 退出。添加一个重复定时器可以可以使 RunLoop 在更长的时间内运行,但需要周期性地触发定时器来唤醒线程,这实际上是另一种轮询形式。相反,输入源等待事件发生,让线程一直处于休眠状态,直到事件发生。

启动 RunLoop

启动 RunLoop 只对应用中的辅助线程是必要的。一个 RunLoop 必须至少有一个输入源或定时器来监控。如果没有添加,RunLoop 将立即退出。

有几种方法可以启动 RunLoop,包括以下方法:

  • 无条件
  • 有固定时间限制
  • 在特定模式下

无条件地进入 RunLoop 是最简单的选择,但也是最不可取的。无条件运行 RunLoop 会将线程放入一个永久循环中,这使你几乎无法控制 RunLoop 本身。你可以添加和删除输入源和定时器,但是停止 RunLoop 的唯一方法是终止它。它无法以自定义模式运行 RunLoop。

逾期无条件地运行 RunLoop,不如使用超时值运行。当你使用超时值时,RunLoop 将一直运行直到事件到达或分配的时间过期。如果事件到达,则将该事件分配给处理程序处理,然后 RunLoop 退出。然后,你的代码可以重新启动 RunLoop 来处理下一个事件。如果分配的时间过期,你可以简单地重新启动 RunLoop,或者使用该时间执行任何需要的内务处理。

除超时值外,还可以使用特定模式运行 RunLoop。模式和超时值不是互斥的,可以在启动 RunLoop 同时使用。模式限制向 RunLoop 交付事件的源的类型,在 《Run Loop Modes》 中进行了更详细的描述。

Listing 3-2 展示了线程主入口例程的骨架版本。本例的关键部分展示了 RunLoop 的基本结构。实际上,你将输入源和定时器添加到 RunLoop 中,然后重复调用一个例程来启动 RunLoop。每次 RunLoop 返回时,你都要检查是否出现了需要退出线程的任何条件。该示例使用了 CoreFoundation RunLoop 例程,以便检查返回结果并确定 RunLoop 退出的原因。如果你使用 Cocoa 并且不需要检查返回值,也可以使用 NSRunLoop 类方法以类似的方式运行 RunLoop。(有关调用 NSRunLoop 类方法的 RunLoop 示例,参见 Listing 3-14)。

Listing 3-2 Running a run loop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (void)skeletonThreadMain
{
// Set up an autorelease pool here if not using garbage collection.
BOOL done = NO;

// Add your sources or timers to the run loop and do any other setup.

do
{
// Start the run loop but return after each source is handled.
SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);

// If a source explicitly stopped the run loop, or if there are no
// sources or timers, go ahead and exit.
if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished))
done = YES;

// Check for any other exit conditions here and set the
// done variable as needed.
}
while (!done);

// Clean up code here. Be sure to release any allocated autorelease pools.
}

递归地运行 RunLoop 是可能的。换句话说,你可以调用 CFRunLoopRunCFRunLoopRunInMode 或任何 NSRunLoop 方法来从输入源或定时器的处理程序例程中启动 RunLoop。这样做时,你可以使用任何希望运行嵌套 RunLoop 的模式,包括外部 RunLoop 所使用的模式。

退出 RunLoop

在处理事件前,有两种方法可以使 RunLoop 退出:

  • 使用超时值配置 RunLoop
  • 告诉 RunLoop 停止

使用一个超时值是首选如果你能管理它的话。指定一个超时值可以让 RunLoop 在退出之前完成所有的正常处理,包括向 RunLoop 观察者发送通知。

使用 CFRunLoopStop 函数显式地停止 RunLoop 将产生类似于超时的效果。RunLoop 发出任何剩余的 RunLoop 通知,然后退出。区别在于,你可以在无条件启动的 RunLoop 上使用这种技术。

尽管移除 RunLoop 的输入源和定时器也可能导致 RunLoop 退出,但这不是停止 RunLoop 的可靠方法。一些系统例程将输入源添加到 RunLoop 中以处理所需的事件。因为你的代码可能不认识这些输入源,所以无法移除它们,这将阻止 RunLoop 退出。

线程安全与 RunLoop 对象

线程安全性取决于使用哪个 API 来操作 RunLoop。CoreFoundation 中的函数通常是线程安全的,可以从任何线程调用。但是,如果你正在执行更改 RunLoop 配置的操作,应该尽可能地从拥有 RunLoop 的线程去执行。

Cocoa NSRunLoop 类并不像它的核心基础类那样本质上是线程安全的。如果你正在使用 NSRunLoop 类修改 RunLoop,那么你应该只从拥有该 RunLoop 的同一线程进行修改。向属于不同线程的 RunLoop 添加输入源或定时器可能会导致代码崩溃或以一种意外的方式运行。

配置 RunLoop 源

下面的小姐将展示如何在 Cocoa 和 CoreFoundation 中设置不同类型的输入源。

定义自定义输入源

创建自定义输入源需要定义一下内容:

  • 你希望输入源处理的信息
  • 一个调度程序例程,让感兴趣的客户端知道如何联系你的输入源
  • 一个处理程序例程,用于执行任何客户端发送的请求
  • 取消例程,试输入源无效

因为你创建了一个自定义输入源来处理自定义信息,所以实际的配置是灵活的。调度器、处理程序和取消例程是自定义源几乎总是需要的关键例程。然而输入源行为的其余大部分都发生在这些处理程序例程之外。例如,由你定义将数据传递到输入源以及将输入源的存在与其他线程通信的机制。

Figure 3-2 显示了自定义输入源的示例配置。在本例中,应用的主里程维护队输入源的引用,该输入源的自定义命令缓冲区以及安装输入源的 RunLoop。当主线程有一个任务要传递给工作线程时,它将一个命令连同工作线程启动任务所需的任何信息一起发送到命令缓冲区。(因为主线程和工作线程的输入源都可以访问命令缓冲区,所以应该同步该访问。)发布命令后,主线程向输入源发出信号,并唤醒工作线程的 RunLoop。在接收到唤醒命令后,RunLoop 调用输入源的处理程序,该处理程序处理在命令缓冲区中找到的命令。

Figure 3-2 Operating a custom input source

下面几节解释前面图中自定义输入源的实现,并展示需要实现的关键代码。

定义输入源

定义自定义输入源需要使用 CoreFoundation 例程来配置 RunLoop 源并将其添加到 RunLoop。尽管基本的处理程序是基于 C 的函数,但这并不妨碍你为这些函数编写包装器,并使用 Objective-C 和 C++ 实现代码体。

Figure 3-2 中介绍的输入源使用 Objective-C 对象管理命令缓冲区并与 RunLoop 协调。Listing 3-3 显示了这个对象的定义。RunLoopSource 对象管理一个命令缓冲区,并使用该缓冲区接收来自其他线程的消息。这个清单还显示了 RunLoopContext 对象的定义,它实际上只是一个容器对象,用于向应用的主线程传递 RunLoopSource 对象及 RunLoop 引用。

Listing 3-3 The custom input source object definition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@interface RunLoopSource : NSObject
{
CFRunLoopSourceRef runLoopSource;
NSMutableArray* commands;
}

- (id)init;
- (void)addToCurrentRunLoop;
- (void)invalidate;

// Handler method
- (void)sourceFired;

// Client interface for registering commands to process
- (void)addCommand:(NSInteger)command withData:(id)data;
- (void)fireAllCommandsOnRunLoop:(CFRunLoopRef)runloop;

@end

// These are the CFRunLoopSourceRef callback functions.
void RunLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFStringRef mode);
void RunLoopSourcePerformRoutine (void *info);
void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode);

// RunLoopContext is a container object used during registration of the input source.
@interface RunLoopContext : NSObject
{
CFRunLoopRef runLoop;
RunLoopSource* source;
}
@property (readonly) CFRunLoopRef runLoop;
@property (readonly) RunLoopSource* source;

- (id)initWithSource:(RunLoopSource*)src andLoop:(CFRunLoopRef)loop;
@end

尽管 Objective-C 代码管理输入源的自定义数据,但是将输入源添加到 RunLoop 需要基于 C 的回调函数。当你实际将 RunLoop 添加到 RunLoop 时,将调用第一个函数,如 Listing 3-4 琐事。因为这个输入源只有一个客户机(主线程),所以它使用调度器函数发送消息,以便在该线程上的应用委托中注册自己。当委托想要与输入源通信时,它使用 RunLoopContext 对象中的信息来这样做。

Listing 3-4 Scheduling a run loop source

1
2
3
4
5
6
7
8
9
void RunLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFStringRef mode)
{
RunLoopSource* obj = (RunLoopSource*)info;
AppDelegate* del = [AppDelegate sharedAppDelegate];
RunLoopContext* theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl];

[del performSelectorOnMainThread:@selector(registerSource:)
withObject:theContext waitUntilDone:NO];
}

最重要的回调例程之一是在输入源有信号时用于处理自定义数据的回调例程。Listing 3-5 显示了 RunLoopSource 对象关联的执行回调例程。这个函数只是将执行此工作的请求转发给 sourceFired 方法,然后该方法处理命令缓冲区出现的任何命令。

Listing 3-5 Performing work in the input source

1
2
3
4
5
void RunLoopSourcePerformRoutine (void *info)
{
RunLoopSource* obj = (RunLoopSource*)info;
[obj sourceFired];
}

如果你使用 CFRunLoopSourceInvalidate 函数将输入源从其 RunLoop 中删除,系统将调用输入源的取消例程。你可以使用此例程通知客户端你的输入源不再有效,它们应该移除对它的任何引用。Listing 3-6 显示了注册到 RunLoopSource 的取消回调例程。这个函数向应用委托发送了另一个 RunLoopContext 对象,但这一次要求委托移除 RunLoop 源的引用。

Listing 3-6 Invalidating an input source

1
2
3
4
5
6
7
8
9
void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode)
{
RunLoopSource* obj = (RunLoopSource*)info;
AppDelegate* del = [AppDelegate sharedAppDelegate];
RunLoopContext* theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl];

[del performSelectorOnMainThread:@selector(removeSource:)
withObject:theContext waitUntilDone:YES];
}

Note: 应用委托的 registerSource:removeSource: 方法的代码展示在 《Coordinating with Clients of the Input Source》 中。

在 RunLoop 中安装输入源

Listing 3-7 展示了 RunLoopSource 类的 initaddToCurrentRunLoop 方法。init 方法创建 CFRunLoopSourceRef 不透明类型,该类型必须实际添加到 RunLoop 中。它将 RunLoopSource 对象本身作为上下文信息传递,这样回调例程就有一个指向该对象的指针。直到工作线程调用 addToCurrentRunLoop 方法(此时调用 RunLoopSourceScheduleRoutine 回调函数),输入源的安装才会发送。一旦输入源被添加到 RunLoop 中,线程就可以运行它的 RunLoop 来等待它。

Listing 3-7 Installing the run loop source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (id)init
{
CFRunLoopSourceContext context = {0, self, NULL, NULL, NULL, NULL, NULL,
&RunLoopSourceScheduleRoutine,
RunLoopSourceCancelRoutine,
RunLoopSourcePerformRoutine};

runLoopSource = CFRunLoopSourceCreate(NULL, 0, &context);
commands = [[NSMutableArray alloc] init];

return self;
}

- (void)addToCurrentRunLoop
{
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopAddSource(runLoop, runLoopSource, kCFRunLoopDefaultMode);
}

协调输入源客户端

要使输入源游泳,需要对其进行操作并从另一个线程发出信号。输入源的全部意义就是让它关联的线程休眠,直到有事情要做为止。这就要求应用中其他线程了解输入源并有方法与之通信。

通知客户端输入源的一种方法是在输入源在 RunLoop 中首次安装时发出注册请求。你可以任意多的客户注册输入源,也可以向某个中央代理注册输入源,然后该代理将输入源提供给感兴趣的客户端。Listing 3-8 显示了应用委托定义的注册方法,并在调用 RunLoopSource 对象的调度器函数时调用改方法。该方法接收 RunLoopSource 对象提供的 RunLoopContext 对象,并将其添加到源列表中。这个清单还显示了在讲输入源从 RunLoop 中移除时用于注销其注册的例程。

Listing 3-8 Registering and removing an input source with the application delegate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)registerSource:(RunLoopContext*)sourceInfo;
{
[sourcesToPing addObject:sourceInfo];
}

- (void)removeSource:(RunLoopContext*)sourceInfo
{
id objToRemove = nil;

for (RunLoopContext* context in sourcesToPing)
{
if ([context isEqual:sourceInfo])
{
objToRemove = context;
break;
}
}

if (objToRemove)
[sourcesToPing removeObject:objToRemove];
}

Note: 调用前面清单中方法的回调函数如 Listing 3-4 和 Listing 3-6 所示。

向输入源发送信号

在将数据传递给输入源之后,客户端必须向源发出信号并唤醒其 RunLoop。向源发出信号可以让 RunLoop 知道源已准备好被处理。由于信号发生时线程可能处于休眠状态,因此应该始终显式的唤醒 RunLoop。如果不这样做,可能会导致处理输入源的延迟。

Listing 3-9 显示了 RunLoopSource 对象的 fireCommandsOnRunLoop 方法。当客户端准备执行缓冲区命令去处理源时,调用此方法。

Listing 3-9 Waking up the run loop

1
2
3
4
5
- (void)fireCommandsOnRunLoop:(CFRunLoopRef)runloop
{
CFRunLoopSourceSignal(runLoopSource);
CFRunLoopWakeUp(runloop);
}

Note: 永远不要尝试通过消息传递自定义输入源去处理 SIGHUP 或其他类型的进程级信号。用于唤醒 RunLoop 的 CoreFoundation 函数 并不是信号安全的,不应在应用的信号处理程序例程中使用。有关信号处理程序例程的更多信息,参见 《sigaction》 手册。

配置定时器源

要创建定时器源,只需创建一个定时器对象并将其调度到 RunLoop 中。在 Cocoa 中,使用 NSTimer 类去创建新的定时器对象,在 CoreFoundation 中使用 CFRunLoopTimerRef 不透明类型。在内部,NSTimer 类只是 CoreFoundation 的一个扩展,它提供了一些方便的特性,比如使用同样的方法创建和调度定时器的能力。

在 Cocoa 中,你使用一下两种类方法创建和调度定时器:

这些方法创建定时器,并在默认模式(NSDefaultRunLoopMode)中将其添加到当前线程的 RunLoop 中。如果需要,还可以手动调度定时器,方式是创建 NSTimer 对象,然后使用 NSRunLoopaddTimer:forMode: 方法将其添加到 RunLoop 中。这两种技术基本上做的是相通的事情,但是为你提供了对定时器配置的不同级别的控制。例如,如果你创建定时器并手动将其添加到 RunLoop 中,啧可以使用默认模式以外的模式来实现。Listing 3-10 展示了如何使用这两种技术创建定时器。第一个定时器的初始延迟为 1s,但之后每 0.1s 定期触发一次。第二个定时器在初始 0.2s 延迟后开始触发,之后每 0.2s 触发一次。

Listing 3-10 Creating and scheduling timers using NSTimer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];

// Create and schedule the first timer.
NSDate* futureDate = [NSDate dateWithTimeIntervalSinceNow:1.0];
NSTimer* myTimer = [[NSTimer alloc] initWithFireDate:futureDate
interval:0.1
target:self
selector:@selector(myDoFireTimer1:)
userInfo:nil
repeats:YES];
[myRunLoop addTimer:myTimer forMode:NSDefaultRunLoopMode];

// Create and schedule the second timer.
[NSTimer scheduledTimerWithTimeInterval:0.2
target:self
selector:@selector(myDoFireTimer2:)
userInfo:nil
repeats:YES];

Listing 3-11 显示了使用 CoreFoundation 函数配置定时器所需的代码。尽管本例没有在上下文中传递任何用户定义的信息,但你可以使用此结构传递定时器所需的任何自定义数据。有关此结构内容的更多信息,参见 《CFRunLoopTimer Reference》

Listing 3-11 Creating and scheduling a timer using Core Foundation

1
2
3
4
5
6
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopTimerContext context = {0, NULL, NULL, NULL, NULL};
CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 0.1, 0.3, 0, 0,
&myCFTimerCallback, &context);

CFRunLoopAddTimer(runLoop, timer, kCFRunLoopCommonModes);

配置基于端口的输入源

Cocoa 和 CoreFoundation 都为线程间或进程间的通信提供了基于端口的对象。以下部分想你展示如何使用不同类型的端口设置端口通信。

配置 NSMachPort 对象

要与 NSMachPort 对象建立本地连接。需要创建一个端口对象并将其添加到 primary 线程的 RunLoop 中。启动辅助线程时,将相同的对象传递给线程的入口函数。辅助线程可以使用相同的对象将消息发送回 primary 线程。

实现主线程代码

Listing 3-12 显示了启动辅助工作线程的 primary 线程代码。因为 Cocoa 框架执行了配置端口和 RunLoop 的许多中间步骤,所以 launchThread 方法明显比它的 CoreFoundation 方法短(Listing 3-17);然而,两者的行为几乎是相同的。一个不同之处在于,这个方法直接发送 NSPort 对象,而不是将本地端口的名称发送给工作线程。

Listing 3-12 Main thread launch method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)launchThread
{
NSPort* myPort = [NSMachPort port];
if (myPort)
{
// This class handles incoming port messages.
[myPort setDelegate:self];

// Install the port as an input source on the current run loop.
[[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];

// Detach the thread. Let the worker release the port.
[NSThread detachNewThreadSelector:@selector(LaunchThreadWithPort:)
toTarget:[MyWorkerClass class] withObject:myPort];
}
}

为了在线程之间建立双向通信通道,你可能希望工作线程在签入消息中向主线程发送自己的本地端口。接收签入消息可能让主线程知道第二个线程一切顺利,还可以向该线程发送更多消息。

Listing 3-13 显示了主线程的 handlePortMessage: 方法。当数据到达线程自己的本地端口时调用此方法。当签入消息到达时,该方法直接从端口消息中检索辅助线程的端口,并将其保存以供以后使用。

Listing 3-13 Handling Mach port messages

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#define kCheckinMessage 100

// Handle responses from the worker thread.
- (void)handlePortMessage:(NSPortMessage *)portMessage
{
unsigned int message = [portMessage msgid];
NSPort* distantPort = nil;

if (message == kCheckinMessage)
{
// Get the worker thread’s communications port.
distantPort = [portMessage sendPort];

// Retain and save the worker port for later use.
[self storeDistantPort:distantPort];
}
else
{
// Handle other messages.
}
}

实现辅助线程代码

对于辅助工作线程,必须配置该线程并使用指定的端口将信息传回 primary 线程。

Listing 3-14 展示了设置工作线程的代码。在为线程创建一个自动释放池后,该方法将创建一个 worker 对象来驱动执行。worker 对象的 sendCheckinMessage: 方法(见 Listing 3-15)为工作线程创建了一个本地端口,并将签入消息发送回主线程。

Listing 3-14 Launching the worker thread using Mach ports

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
+(void)LaunchThreadWithPort:(id)inData
{
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];

// Set up the connection between this thread and the main thread.
NSPort* distantPort = (NSPort*)inData;

MyWorkerClass* workerObj = [[self alloc] init];
[workerObj sendCheckinMessage:distantPort];
[distantPort release];

// Let the run loop process things.
do
{
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
beforeDate:[NSDate distantFuture]];
}
while (![workerObj shouldExit]);

[workerObj release];
[pool release];
}

在使用 NSMachPort 时,本地线程和远端线程可以使用相同的端口对象进行线程之间的单向通信。换句话说,由一个线程创建的本地端口对象成为另一个线程的远程端口对象。

Listing 3-15 显示了辅助线程的签入例程。该方法为将来的通信设置自己的本地端口,然后将签入消息发送回主线程。该方法使用 LaunchThreadWithPort: 方法中接收到的端口对象作为消息的目标。

Listing 3-15 Sending the check-in message using Mach ports

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Worker thread check-in method
- (void)sendCheckinMessage:(NSPort*)outPort
{
// Retain and save the remote port for future use.
[self setRemotePort:outPort];

// Create and configure the worker thread port.
NSPort* myPort = [NSMachPort port];
[myPort setDelegate:self];
[[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];

// Create the check-in message.
NSPortMessage* messageObj = [[NSPortMessage alloc] initWithSendPort:outPort
receivePort:myPort components:nil];

if (messageObj)
{
// Finish configuring the message and send it immediately.
[messageObj setMsgId:setMsgid:kCheckinMessage];
[messageObj sendBeforeDate:[NSDate date]];
}
}

配置 NSMessagePort 对象

要与 NSMessagePort 对象建立本地连接,不能简单地在线程之间传递端口对象。远程消息端口必须按名称获取。要在 Cocoa 中实现这一点,需要使用特定的名称注册本地端口然后将该名称传递给远端线程,以便它能获得合适的端口对象进行通信。如果你想用消息端口,Listing 3-16 显示了端口创建和注册的过程。

Listing 3-16 Registering a message port

1
2
3
4
5
6
7
8
9
10
NSPort* localPort = [[NSMessagePort alloc] init];

// Configure the object and add it to the current run loop.
[localPort setDelegate:self];
[[NSRunLoop currentRunLoop] addPort:localPort forMode:NSDefaultRunLoopMode];

// Register the port using a specific name. The name must be unique.
NSString* localPortName = [NSString stringWithFormat:@"MyPortName"];
[[NSMessagePortNameServer sharedInstance] registerPort:localPort
name:localPortName];

在 CoreFoundation 中配置基于端口的输入源

本节展示如何使用 CoreFoundation 在应用主线程与工作线程间设置双向通道。

Listing 3-17 显示了应用的主线程启动工作线程调用的代码。代码所做的第一件事是设置 CFMessagePortRef 不透明类型,以监听来自工作线程的消息。工作线程需要端口名称来进行连接,以便将字符串值传递到工作线程的入口函数。端口名称通常在当前用户上下文中是唯一的;否则可能会遇到冲突。

Listing 3-17 Attaching a Core Foundation message port to a new thread

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#define kThreadStackSize        (8 *4096)

OSStatus MySpawnThread()
{
// Create a local port for receiving responses.
CFStringRef myPortName;
CFMessagePortRef myPort;
CFRunLoopSourceRef rlSource;
CFMessagePortContext context = {0, NULL, NULL, NULL, NULL};
Boolean shouldFreeInfo;

// Create a string with the port name.
myPortName = CFStringCreateWithFormat(NULL, NULL, CFSTR("com.myapp.MainThread"));

// Create the port.
myPort = CFMessagePortCreateLocal(NULL,
myPortName,
&MainThreadResponseHandler,
&context,
&shouldFreeInfo);

if (myPort != NULL)
{
// The port was successfully created.
// Now create a run loop source for it.
rlSource = CFMessagePortCreateRunLoopSource(NULL, myPort, 0);

if (rlSource)
{
// Add the source to the current run loop.
CFRunLoopAddSource(CFRunLoopGetCurrent(), rlSource, kCFRunLoopDefaultMode);

// Once installed, these can be freed.
CFRelease(myPort);
CFRelease(rlSource);
}
}

// Create the thread and continue processing.
MPTaskID taskID;
return(MPCreateTask(&ServerThreadEntryPoint,
(void*)myPortName,
kThreadStackSize,
NULL,
NULL,
NULL,
0,
&taskID));
}

安装端口并启动线程后,主线程可以在等待线程签入时继续其常规执行。当签入消息到达时,它被派发到主线程的 MainThreadResponseHandler 函数,如 Listing 3-18 所示。此函数提取工作线程的端口名称,并为将来的通信创建通道。

Listing 3-18 Receiving the checkin message

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#define kCheckinMessage 100

// Main thread port message handler
CFDataRef MainThreadResponseHandler(CFMessagePortRef local,
SInt32 msgid,
CFDataRef data,
void* info)
{
if (msgid == kCheckinMessage)
{
CFMessagePortRef messagePort;
CFStringRef threadPortName;
CFIndex bufferLength = CFDataGetLength(data);
UInt8* buffer = CFAllocatorAllocate(NULL, bufferLength, 0);

CFDataGetBytes(data, CFRangeMake(0, bufferLength), buffer);
threadPortName = CFStringCreateWithBytes (NULL, buffer, bufferLength, kCFStringEncodingASCII, FALSE);

// You must obtain a remote message port by name.
messagePort = CFMessagePortCreateRemote(NULL, (CFStringRef)threadPortName);

if (messagePort)
{
// Retain and save the thread’s comm port for future reference.
AddPortToListOfActiveThreads(messagePort);

// Since the port is retained by the previous function, release
// it here.
CFRelease(messagePort);
}

// Clean up.
CFRelease(threadPortName);
CFAllocatorDeallocate(NULL, buffer);
}
else
{
// Process other messages.
}

return NULL;
}

配置好主线程之后,剩下的事就是新创建的工作线程创建自己的端口并签入。Listing 3-19 展示了工作线程的入口点函数。该函数提取主线程的端口名称,并使用它创建一个返回到主线程的远程连接。然后,该函数为自己创建一个本地端口,将端口安装在远程的 RunLoop 上,并向主线程发送包含本地端口名称的签入消息。

Listing 3-19 Setting up the thread structures

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
OSStatus ServerThreadEntryPoint(void* param)
{
// Create the remote port to the main thread.
CFMessagePortRef mainThreadPort;
CFStringRef portName = (CFStringRef)param;

mainThreadPort = CFMessagePortCreateRemote(NULL, portName);

// Free the string that was passed in param.
CFRelease(portName);

// Create a port for the worker thread.
CFStringRef myPortName = CFStringCreateWithFormat(NULL, NULL, CFSTR("com.MyApp.Thread-%d"), MPCurrentTaskID());

// Store the port in this thread’s context info for later reference.
CFMessagePortContext context = {0, mainThreadPort, NULL, NULL, NULL};
Boolean shouldFreeInfo;
Boolean shouldAbort = TRUE;

CFMessagePortRef myPort = CFMessagePortCreateLocal(NULL,
myPortName,
&ProcessClientRequest,
&context,
&shouldFreeInfo);

if (shouldFreeInfo)
{
// Couldn't create a local port, so kill the thread.
MPExit(0);
}

CFRunLoopSourceRef rlSource = CFMessagePortCreateRunLoopSource(NULL, myPort, 0);
if (!rlSource)
{
// Couldn't create a local port, so kill the thread.
MPExit(0);
}

// Add the source to the current run loop.
CFRunLoopAddSource(CFRunLoopGetCurrent(), rlSource, kCFRunLoopDefaultMode);

// Once installed, these can be freed.
CFRelease(myPort);
CFRelease(rlSource);

// Package up the port name and send the check-in message.
CFDataRef returnData = nil;
CFDataRef outData;
CFIndex stringLength = CFStringGetLength(myPortName);
UInt8* buffer = CFAllocatorAllocate(NULL, stringLength, 0);

CFStringGetBytes(myPortName,
CFRangeMake(0,stringLength),
kCFStringEncodingASCII,
0,
FALSE,
buffer,
stringLength,
NULL);

outData = CFDataCreate(NULL, buffer, stringLength);

CFMessagePortSendRequest(mainThreadPort, kCheckinMessage, outData, 0.1, 0.0, NULL, NULL);

// Clean up thread data structures.
CFRelease(outData);
CFAllocatorDeallocate(NULL, buffer);

// Enter the run loop.
CFRunLoopRun();
}

一旦进入 RunLoop,所有发送到远程端口的未来事件都由 ProcessClientRequest 函数处理。该函数的实现取决于线程所做的工作类型,这里没有显示。