运行循环是与线程相关的基础架构的一部分。一个运行循环是指用于安排工作,并协调接收传入事件的事件处理循环。运行循环的目的是在有工作要做的时候保持你的线程忙,当没有时候让线程进入睡眠状态。运行循环管理不是完全自动的。您仍然必须设计线程的代码,以便在适当的时候启动运行循环,并响应传入的事件。Cocoa和Core Foundation都提供运行循环对象来帮助你配置和管理线程的运行循环。您的应用程序不需要显式创建这些对象; 每个线程(包括应用程序的主线程)都有一个关联的运行循环对象。但是,只有次要线程需要显式运行其运行循环。应用程序框架自动设置并在主线程上运行运行循环,作为应用程序启动过程的一部分。以下各节提供了有关运行循环的更多信息以及如何为应用程序配置它们。有关运行循环对象的更多信息,请参阅NSRunLoop类参考和CFRunLoop参考。

目录

  1. 一个运行循环的解剖
    1. 运行循环模式
    2. 输入源
    3. 基于端口的来源
    4. 自定义输入源
    5. Cocoa执行选择来源
    6. Timer Sources
    7. Run Loop Observers
    8. 事件的运行循环顺序
  2. 何时使用运行循环?
  3. 使用运行循环对象
    1. 获取运行循环对象
    2. 配置运行循环
    3. 启动运行循环
    4. 退出运行循环
    5. 线程安全和运行循环对象
  4. 配置运行循环源
    1. 定义一个自定义的输入源
      1. 定义输入源
      2. 在运行循环中安装输入源
      3. 与输入源的客户端协调
      4. 发信号输入源
    2. 配置定时器源
    3. 配置基于端口的输入源
      1. 配置一个NSMachPort对象
        1. 实现主线程代码
        2. 实现次要线程代码
      2. 配置一个NSMessagePort对象
      3. 在Core Foundation中配置基于端口的输入源

一个运行循环的解剖

运行循环非常像它的名字的声音。这是一个循环,你的线程进入并用来运行事件处理程序来响应传入的事件。您的代码提供了用于实现循环,换句话说,你的代码提供了运行的实际环部控制语句while或for循环驱动运行循环。在循环中,使用运行循环对象来“运行”接收事件的事件处理代码,并调用已安装的处理程序。
运行循环接收来自两种不同类型的源的事件。输入源提供异步事件,通常是来自另一个线程或不同应用程序的消息。定时器源提供同步事件,发生在预定时间或重复间隔。这两种类型的源使用特定于应用程序的处理程序来处理事件到达时。
图3-1显示了运行循环和各种来源的概念结构。输入源将异步事件传递给相应的处理程序,并使该runUntilDate:方法(在线程的关联NSRunLoop对象上调用)退出。定时器源将事件传递到其处理程序例程,但不会导致运行循环退出。

除了处理输入源之外,运行循环还会生成有关运行循环行为的通知。注册的运行循环观察者可以接收这些通知,并使用它们在线程上执行额外的处理。您可以使用Core Foundation在线程上安装运行循环观察器。

以下部分提供了有关运行循环的组件和运行模式的更多信息。他们还描述了事件处理期间在不同时间生成的通知。

运行循环模式

一个运行的循环模式是输入源和定时器的集合进行监测和运行循环观察员集合通知。每次运行运行循环时,都可以指定(显式或隐式)特定的“模式”来运行。在运行循环的过程中,只有与该模式相关的源被监视并被允许传送它们的事件。(类似地,只有与该模式相关的观察者被通知运行循环的进度。)与其他模式相关的源保持到任何新的事件,直到随后以适当的模式通过循环。
在你的代码中,你可以通过名字来识别模式。Cocoa和Core Foundation都定义了一个默认模式和几个常用模式,以及用于在代码中指定这些模式的字符串。您可以通过为模式名称指定自定义字符串来定义自定义模式。尽管分配给自定义模式的名称是任意的,但这些模式的内容却不是。您必须确保将一个或多个输入源,定时器或运行循环观察器添加到您创建的任何模式中,以使其有用。
您可以使用模式在特定的运行循环中过滤掉不需要的来源的事件。大多数情况下,您需要在系统定义的“默认”模式下运行您的运行循环。然而,模态面板可能以“模态”模式运行。在这种模式下,只有与模式面板相关的源才会将事件传递给线程。对于辅助线程,可以使用自定义模式来防止低优先级的源在时间关键型操作期间传递事件。

注意:模式基于事件的来源而不是事件的类型进行区分。例如,您不会使用模式来匹配仅鼠标按下事件或仅键盘事件。您可以使用模式来侦听不同的端口集,临时挂起定时器,或者更改当前正在监视的源和运行循环观察器。

下表列出了Cocoa和Core Foundation定义的标准模式以及何时使用该模式的描述。名称列列出了用于在代码中指定模式的实际常量。

模式 名称 描述
默认 NSDefaultRunLoopMode (Cocoa) kCFRunLoopDefaultMode (Core Foundation) 默认模式是大多数操作使用的模式。大多数情况下,您应该使用此模式启动运行循环并配置输入源。
连接 NSConnectionReplyMode (可可) Cocoa使用这个模式和NSConnection对象一起来监视回复。你应该很少需要自己使用这个模式。
语气 NSModalPanelRunLoopMode(Cocoa) 可可使用这种模式来识别用于模态面板的事件。
事件跟踪 NSEventTrackingRunLoopMode(Cocoa) Cocoa使用这种模式来限制鼠标拖拽循环和其他类型的用户界面追踪循环中的传入事件。
共同的模式 NSRunLoopCommonModes (Cocoa)kCFRunLoopCommonModes (Core Foundation) 这是一个可配置的常用模式组。将输入源与此模式关联也会将其与组中的每个模式相关联。对于Cocoa应用程序,这个集合默认包括默认,模态和事件跟踪模式。核心基础最初只包含默认模式。您可以使用该CFRunLoopAddCommonMode功能将自定义模式添加到该设置。

输入源

输入源以异步方式向您的线程传递事件。事件的来源取决于输入源的类型,这通常是两类之一。基于端口的输入源监视您的应用程序的Mach端口。自定义输入源监视自定义的事件源。就你的运行循环而言,输入源是基于端口的还是自定义的都不重要。系统通常实现两种类型的输入源,您可以按原样使用。这两个来源唯一的区别是它们是如何发出信号的。内核自动发送基于端口的源,自定义源必须从另一个线程手动发送信号。
创建输入源时,将其分配给运行循环的一个或多个模式。模式会影响在任何特定时刻监视哪些输入源。大多数情况下,您在默认模式下运行运行循环,但您也可以指定自定义模式。如果输入源不处于当前监控的模式,则会生成所有生成的事件,直到运行循环以正确的模式运行。
以下各节介绍一些输入源。

基于端口的来源

Cocoa和Core Foundation为使用端口相关的对象和函数创建基于端口的输入源提供了内置的支持。例如,在Cocoa中,您根本不需要直接创建输入源。您只需创建一个端口对象并使用将该NSPort端口添加到运行循环的方法。port对象为您处理所需输入源的创建和配置。
在Core Foundation中,您必须手动创建端口及其运行循环源。在这两种情况下,您使用的端口类型不透明(相关的功能CFMachPortRef,CFMessagePortRef或CFSocketRef)创建合适的对象。
有关如何设置和配置基于端口的自定义源的示例,请参阅配置基于端口的输入源。

自定义输入源

要创建自定义输入源,您必须使用与CFRunLoopSourceRefCore Foundation中的不透明类型关联的函数。您可以使用多个回调函数来配置自定义输入源。Core Foundation在不同的点调用这些函数来配置源代码,处理所有传入的事件,并在源代码从运行循环中移除时拆除源代码。
除了在事件到达时定义自定义源的行为之外,还必须定义事件传递机制。这部分源代码运行在一个单独的线程上,负责为输入源提供数据,并在数据准备好处理时通知它。事件传递机制取决于您,但不必过于复杂。
有关如何创建自定义输入源的示例,请参阅定义自定义输入源。有关自定义输入源的参考信息,另请参阅CFRunLoopSource参考。

Cocoa执行选择来源

除了基于端口的源代码之外,Cocoa还定义了一个自定义输入源,允许您在任何线程上执行选择器。像基于端口的源一样,执行选择器请求在目标线程上被序列化,减轻了在一个线程上运行多个方法时可能发生的许多同步问题。与基于端口的源不同,执行选择器源在执行选择器之后将自身从运行循环中移除。

注意:在OS X v10.5之前,执行选择器源主要用于向主线程发送消息,但在OS X v10.5及更高版本和iOS中,可以使用它们向任何线程发送消息。

在另一个线程上执行选择器时,目标线程必须有一个活动的运行循环。对于你创建的线程,这意味着等待直到你的代码明确地启动运行循环。因为主线程启动自己的运行循环,所以只要应用程序调用applicationDidFinishLaunching:应用程序委托的方法,就可以开始在该线程上发出调用 。运行循环每次通过循环处理所有排队的执行选择器调用,而不是在每个循环迭代中处理一个。
下表列出了NSObject可用于在其他线程上执行选择器的方法。因为这些方法已经声明了NSObject,所以你可以在任何有权访问Objective-C对象的线程中使用它们,包括POSIX线程。这些方法实际上不会创建一个新线程来执行选择器。

方法 描述
performSelectorOnMainThread: withObject:waitUntilDone: performSelectorOnMainThread: withObject:waitUntilDone:modes: 在该线程的下一个运行循环中执行应用程序主线程上的指定选择器。这些方法给你选择阻塞当前的线程,直到执行选择器。
performSelector:onThread: withObject:waitUntilDone: performSelector:onThread: withObject:waitUntilDone:modes: 在您拥有NSThread对象的任何线程上执行指定的选择器。这些方法给你选择阻塞当前的线程,直到执行选择器。
performSelector: withObject:afterDelay: performSelector: withObject:afterDelay:inModes: 在下一个运行循环周期和可选延迟周期之后,在当前线程上执行指定的选择器。由于它一直等到下一个运行循环执行选择器,所以这些方法提供了从当前正在执行的代码中自动缩短的延迟。多个排队选择器按照排队顺序依次执行。
cancelPreviousPerform RequestsWithTarget: cancelPreviousPerform RequestsWithTarget:selector:object: 允许您使用performSelector: withObject: afterDelay: or performSelector: withObject: afterDelay: inModes:方法取消发送到当前线程的消息。

Timer Sources

定时器源在未来的预设时间将事件同步传递给您的线程。定时器是线程通知自己做某事的一种方式。例如,搜索字段可以使用定时器来在用户的连续击键之间经过一定的时间后启动自动搜索。这个延迟时间的使用使用户有机会在开始搜索之前输入尽可能多的所需搜索字符串。
虽然它会生成基于时间的通知,但计时器不是实时机制。像输入源一样,定时器与运行循环的特定模式相关联。如果一个定时器没有处于当前被运行循环监视的模式,那么只有在定时器的一种支持模式下运行循环时才会触发定时器。同样的,如果一个定时器在运行循环处于执行处理程序的过程中触发,定时器会等待下一次通过运行循环来调用它的处理程序。如果运行循环根本没有运行,定时器不会启动。
您可以将定时器配置为仅生成一次或重复生成事件。重复计时器会根据预定的开火时间而不是实际的开火时间自动重新安排时间。例如,如果计时器在特定的时间以及之后的每5秒钟发射一次,则即使实际的发射时间被延迟,计划的发射时间将总是以原来的5秒的时间间隔下降。如果开火时间延迟太多,以至于错过了一个或多个预定的开火时间,则计时器仅在错过的时间段内被触发一次。在错过的时间点火之后,定时器重新计划下一个预定的点火时间。
有关配置定时器源的更多信息,请参阅配置定时器源。有关参考信息,请参阅NSTimer类参考或CFRunLoopTimer参考。

Run Loop Observers

与发生适当异步事件或同步事件时触发的源相比,运行循环观察器在运行循环本身的执行期间会在特定位置触发。您可以使用运行循环观察程序来准备线程来处理给定事件,或者在线程进入睡眠之前准备线程。您可以将运行循环观察程序与运行循环中的以下事件相关联:
• 运行循环的入口。
• 运行循环即将处理计时器时。
• 运行循环即将处理输入源时。
• 当运行循环即将进入睡眠状态时。
• 运行循环唤醒时,但在处理唤醒它的事件之前。
• 从运行循环退出。
您可以使用Core Foundation将运行循环观察器添加到应用程序。要创建一个运行循环观察者,可以创建一个CFRunLoopObserverRef不透明类型的新实例。此类型跟踪您的自定义回调函数和它感兴趣的活动。
类似于定时器,运行循环观察者可以使用一次或重复使用。一次性观察者在火灾发生后从运行循环中移除,而重复的观察者则保持连接状态。您可以指定观察者在创建时是运行一次还是反复运行。
有关如何创建运行循环观察程序的示例,请参阅配置运行循环。有关参考信息,请参阅CFRunLoopObserver参考。

事件的运行循环顺序

每次运行它时,线程的运行循环都会处理挂起的事件,并为任何附加的观察者生成通知。它的执行顺序非常具体,如下所示:

  1. 通知观察者已经输入了运行循环。
  2. 通知观察者,任何准备好的计时器即将开火。
  3. 通知观察者,任何不是基于端口的输入源都将被触发。
  4. 触发任何可以触发的非基于端口的输入源。
  5. 如果基于端口的输入源已准备就绪并正在等待触发,请立即处理该事件。转到第9步。
  6. 通知观察者线程即将睡眠。
  7. 使线程进入睡眠状态,直到发生以下事件之一:
    • 一个事件到达一个基于端口的输入源。
    • 计时器启动。
    • 为运行循环设置的超时值已过期。
    • 运行循环明确地被唤醒。
  8. 通知观察者线程刚刚醒来。
  9. 处理未决事件。
    • 如果用户定义的定时器触发,则处理定时器事件并重新启动循环。转到第2步。
    • 如果输入源被触发,则交付事件。
    • 如果运行循环被明确唤醒但尚未超时,请重新启动循环。转到第2步。
  10. 通知观察者运行循环已经退出。

由于定时器和输入源的观察者通知在这些事件实际发生之前就被发送,所以通知的时间和实际事件的时间之间可能存在差距。如果这些事件之间的时间很关键,则可以使用睡眠和从睡眠中醒来的通知来帮助您关联实际事件之间的时间。
由于定时器和其他周期性事件是在运行运行循环时交付的,因此绕过该循环会中断这些事件的传递。当您通过输入一个循环并重复地从应用程序请求事件来实现鼠标跟踪例程时,就会出现这种行为的典型示例。由于您的代码是直接抓取事件,而不是让应用程序正常调度这些事件,因此在您的鼠标跟踪例程退出并将控制权返回给应用程序之前,激活的定时器将无法触发。
运行循环可以使用运行循环对象显式唤醒。其他事件也可能导致运行循环被唤醒。例如,添加另一个非基于端口的输入源会唤醒运行循环,以便可以立即处理输入源,而不是等待其他事件发生。

何时使用运行循环?

唯一需要明确运行运行循环的时候是为应用程序创建辅助线程。应用程序主线程的运行循环是一个关键的基础设施。因此,应用程序框架提供了运行主应用程序循环的代码,并自动启动该循环。所述run的方法UIApplication在IOS(或NSApplication在OS X)启动应用程序的主循环的正常启动序列的一部分。如果您使用Xcode模板项目来创建您的应用程序,您不应该显式调用这些例程。
对于辅助线程,您需要确定是否需要运行循环,如果是,则自行配置并启动它。在任何情况下,您都不需要启动线程的运行循环。例如,如果您使用线程来执行一些长时间运行和预定义的任务,则可以避免启动运行循环。运行循环适用于需要更多与线程交互的情况。例如,如果您打算执行以下任一操作,则需要启动一个运行循环:

  • 使用端口或自定义输入源与其他线程通信。
  • 在线程上使用计时器。
  • 使用performSelectorCocoa应用程序中的任何…方法。
  • 保持线程执行定期任务。

如果您选择使用运行循环,则配置和设置很简单。就像所有的线程编程一样,你应该有一个在适当的情况下退出你的辅助线程的计划。通过让它退出而不是强制终止一个线程总是更好。有关如何配置和退出运行循环的信息,请参见使用运行循环对象。

使用运行循环对象

运行循环对象提供了将输入源,定时器和运行循环观察器添加到运行循环并运行的主界面。每个线程都有一个与之关联的运行循环对象。在Cocoa中,这个对象是NSRunLoop类的一个实例。在低级应用程序中,它是一个指向CFRunLoopRef不透明类型的指针。

获取运行循环对象

要获取当前线程的运行循环,请使用以下其中一项:

  • 在Cocoa应用程序中,使用currentRunLoop类的方法NSRunLoop来检索一个NSRunLoop对象。
  • 使用该CFRunLoopGetCurrent功能。

虽然它们不是免费的桥接类型,但是在需要时可以CFRunLoopRef从NSRunLoop对象中获取不透明的类型。本NSRunLoop类定义了一个getCFRunLoop返回的方法CFRunLoopRef类型,你可以传递给Core Foundation的例程。由于两个对象都引用相同的运行循环,因此可以根据需要将调用混合到NSRunLoop对象和CFRunLoopRef不透明类型。

配置运行循环

在辅助线程上运行运行循环之前,必须至少添加一个输入源或计时器。如果运行循环没有任何监视源,当您尝试运行它时,它会立即退出。有关如何将源添加到运行循环的示例,请参阅配置运行循环源。
除了安装源代码之外,还可以安装运行循环观察器,并使用它们来检测运行循环的不同执行阶段。要安装一个运行循环观察者,可以创建一个CFRunLoopObserverRef不透明类型,并使用该CFRunLoopAddObserver函数将其添加到运行循环中。运行循环观察者必须使用Core Foundation创建,即使对于Cocoa应用程序也是如此。
下面代码显示了将运行循环观察器连接到其运行循环的线程的主例程。该示例的目的是向您展示如何创建一个运行循环观察器,因此代码只需设置一个运行循环观察器即可监视所有运行循环活动。基本处理程序例程(未显示)只是在处理计时器请求时记录运行循环活动。

清单3-1:创建一个运行循环观察者
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
- (void)threadMain
{
//应用程序使用垃圾回收,所以不需要autorelease池。
NSRunLoop * myRunLoop = [NSRunLoop currentRunLoop];
//创建一个运行循环观察器并将其附加到运行循环中。
CFRunLoopObserverContext context = {0selfNULLNULLNULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,YES0,&myRunLoopObserver,&context);
if (observer)
{
CFRunLoopRef cfLoop = [myRunLoop getCFRunLoop];
CFRunLoopAddObserver(cfLoop,observer,kCFRunLoopDefaultMode);
}
//创建和计划定时器。
[NSTimer scheduledTimerWithTimeInterval:0.1 target:self
selector:@selector(doFireTimer:) userInfo:nil repeats:YES];
NSInteger loopCount = 10;
do
{
//运行循环10次,让定时器启动。
[myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
loopCount--;
}
while(loopCount);
}

为长效线程配置运行循环时,最好添加至少一个输入源来接收消息。尽管只能连接一个定时器才能进入运行循环,但是一旦定时器触发,它通常是无效的,这会导致运行循环退出。附加一个重复计时器可以使运行循环运行一段较长的时间,但会涉及周期性地启动计时器来唤醒线程,这实际上是另一种轮询形式。相反,输入源等待事件发生,保持线程睡着,直到它发生。

启动运行循环

启动运行循环仅适用于应用程序中的辅助线程。运行循环必须至少有一个输入源或定时器来监视。如果没有连接,运行循环立即退出。
有几种启动运行循环的方式,包括:

  • 无条件
  • 有一个设定的时间限制
  • 在特定模式下

无条件地进入你的运行循环是最简单的选择,但也是最不可取的。无条件地运行你的运行循环把线程放到一个永久的循环中,这使你很少控制运行循环本身。您可以添加和删除输入源和定时器,但停止运行循环的唯一方法是杀死它。在自定义模式下也无法运行运行循环。
不要无条件地运行一个运行循环,最好用一个超时值来运行运行循环。当使用超时值时,运行循环运行直到事件到达或分配的时间到期。如果事件到达,则将该事件分派给处理程序进行处理,然后运行循环退出。您的代码然后可以重新启动运行循环来处理下一个事件。如果分配的时间到期,您可以简单地重新启动运行循环或使用时间做任何需要的管家。
除了超时值之外,还可以使用特定模式运行运行循环。模式和超时值不是相互排斥的,在启动运行循环时都可以使用。模式限制将事件传递到运行循环的源类型,在运行循环模式中有更详细的描述。
程序清单3-2显示了一个线程主入口例程的框架版本。这个例子的关键部分显示了运行循环的基本结构。本质上,您将输入源和定时器添加到运行循环,然后重复调用其中一个例程来启动运行循环。每次运行循环例程返回时,都会检查是否有任何可能导致退出线程的情况。该示例使用Core Foundation运行循环例程,以便它可以检查返回结果并确定运行循环退出的原因。NSRunLoop如果您使用Cocoa并且不需要检查返回值,也可以使用类的方法以类似的方式运行运行循环。(有关调用NSRunLoop类的方法的运行循环的示例,请参见清单3-14)。

清单3-2:运行一个运行循环
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)skeletonThreadMain
{
// 如果不使用垃圾回收,在这里设置一个autorelease池。
BOOL done = NO;
// 将你的源或定时器添加到运行循环中,并执行其他任何设置。
do
{
// 启动运行循环,但在处理完每个源之后返回。
SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);
// 如果一个源明确地停止了运行循环,或者没有
// 源或定时器,继续并退出。
if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished))
done = YES;
// 检查任何其他退出条件,并设置
// 根据需要完成变量
}
while (!done);
// 在这里清理代码 一定要释放任何分配的autorelease池。
}

可以递归地运行一个运行循环。换句话说,您可以调用CFRunLoopRun,CFRunLoopRunInMode或NSRunLoop从输入源或定时器的处理程序例程中启动运行循环的任何方法。这样做时,可以使用任何您想要运行嵌套运行循环的模式,包括外部循环使用的模式。

退出运行循环

在处理事件之前,有两种方法可以使运行循环退出:

  • 配置运行循环以超时值运行。
  • 告诉运行循环停止。

如果可以管理它,使用超时值肯定是首选。指定一个超时值可以让运行循环完成所有的正常处理,包括在退出之前传递通知来运行循环观察器。
显式停止运行循环CFRunLoopStop会产生类似于超时的结果。运行循环发出任何剩余的运行循环通知,然后退出。不同的是,您可以在无条件启动的运行循环中使用这种技术。
尽管删除运行循环的输入源和定时器也可能导致运行循环退出,但这不是停止运行循环的可靠方法。一些系统例程将输入源添加到运行循环中以处理所需的事件。因为你的代码可能不知道这些输入源,所以它将无法删除它们,这将阻止运行循环退出。

线程安全和运行循环对象

线程安全性取决于您使用哪个API来操作运行循环。Core Foundation中的函数通常是线程安全的,可以从任何线程调用。但是,如果您正在执行的操作会改变运行循环的配置,则尽可能从拥有运行循环的线程执行此操作仍然是一个好习惯。
可可NSRunLoop类并不像其核心基础对象那样天生就是线程安全的。如果您正在使用NSRunLoop该类来修改您的运行循环,则只能从拥有该运行循环的同一个线程执行此操作。将输入源或计时器添加到属于不同线程的运行循环中可能会导致代码崩溃或以意外的方式运行。

配置运行循环源

以下部分显示了如何在Cocoa和Core Foundation中设置不同类型的输入源的示例。

定义一个自定义的输入源

创建自定义输入源包括定义以下内容:

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

由于您创建了一个自定义输入源来处理自定义信息,因此实际配置的设计灵活。调度程序,处理程序和取消例程是几乎总是需要用于自定义输入源的关键例程。然而,大部分输入源行为的其余部分都发生在这些处理程序之外。例如,您可以定义将数据传递到输入源的机制,并将输入源的存在传递给其他线程。
图3-2显示自定义输入源的示例配置。在这个例子中,应用程序的主线程保持对输入源,该输入源的自定义命令缓冲区以及安装输入源的运行循环的引用。当主线程有一个任务想要切换到工作线程时,它将一个命令发送到命令缓冲区以及工作线程启动任务所需的任何信息。(因为工作线程的主线程和输入源都可以访问命令缓冲区,所以访问必须是同步的。)一旦命令发布,主线程就会发信号通知输入源并唤醒工作线程的运行循环。在收到唤醒命令后,运行循环会调用输入源的处理程序,该输入源处理命令缓冲区中的命令。
图3-2操作自定义输入源
以下各节将解释上图中自定义输入源的实现,并显示您需要实现的关键代码。

定义输入源

定义自定义输入源需要使用Core Foundation例程来配置运行循环源并将其附加到运行循环。虽然基本的处理程序是基于C语言的函数,但是这并不妨碍您为这些函数编写包装,并使用Objective-C或C ++来实现代码的主体。
图3-2中介绍的输入源使用Objective-C对象来管理命令缓冲区并与运行循环协调。清单3-3显示了这个对象的定义。该RunLoopSource对象管理命令缓冲区,并使用该缓冲区接收来自其他线程的消息。此列表还显示了RunLoopContext对象的定义,它实际上只是一个用于传递RunLoopSource对象的容器对象,并且是对应用程序主线程的运行循环引用。

清单3-3:自定义输入源对象定义
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
@interface RunLoopSource : NSObject
{
CFRunLoopSourceRef runLoopSource;
NSMutableArray* commands;
}
- (id)init;
- (void)addToCurrentRunLoop;
- (void)invalidate;
// Handler方法
- (void)sourceFired;
// 用于注册要处理的命令的客户端界面
- (void)addCommand:(NSInteger)command withData:(id)data;
- (void)fireAllCommandsOnRunLoop:(CFRunLoopRef)runloop;
@end
// 这些是CFRunLoopSourceRef回调函数。
void RunLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFStringRef mode);
void RunLoopSourcePerformRoutine (void *info);
void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode);
// RunLoopContext是注册输入源期间使用的容器对象。
@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代码管理输入源的自定义数据,但将输入源附加到运行循环中需要使用基于C的回调函数。当你将运行循环源连接到你的运行循环时,将会调用第一个函数,如代码3-4所示。因为这个输入源只有一个客户端(主线程),所以它使用调度程序函数来发送一个消息来向该线程上的应用程序委托注册自己。代表想要与输入源进行通信时,它使用RunLoopContext对象中的信息来执行此操作。

清单3-4:调度运行循环源
1
2
3
4
5
6
7
8
void RunLoopSourceScheduleRoutine(void * info,CFRunLoopRef rl,CFStringRef模式)
{
RunLoopSource * obj =(RunLoopSource *)info;
AppDelegate * del = [AppDelegate sharedAppDelegate];
RunLoopContext * theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl];
[del performSelectorOnMainThread:@selector(registerSource :)
withObject:theContext waitUntilDone:NO];
}

其中一个最重要的回调函数是用于在输入源发送信号时处理自定义数据的回调函数。清单3-5显示了与RunLoopSource对象关联的执行回调例程。这个函数简单地把请求做的工作转发给sourceFired方法,然后处理命令缓冲区中的所有命令。

清单3-5:在输入源中执行工作
1
2
3
4
5
void RunLoopSourcePerformRoutine(void * info)
{
RunLoopSource * obj =(RunLoopSource *)info;
[obj sourceFired];
}

如果你使用CFRunLoopSourceInvalidate函数从运行循环中删除你的输入源,系统调用你的输入源的取消例程。您可以使用此例程来通知客户端您的输入源不再有效,并且应该删除对它的任何引用。 代码清单3-6显示了在RunLoopSource对象中注册的取消回调例程。此函数将另一个RunLoopContext对象发送给应用程序委托,但是这次要求委托移除对运行循环源的引用。

清单3-6:使输入源无效
1
2
3
4
5
6
7
8
void RunLoopSourceCancelRoutine(void * info,CFRunLoopRef rl,CFStringRef模式)
{
RunLoopSource * obj =(RunLoopSource *)info;
AppDelegate * del = [AppDelegate sharedAppDelegate];
RunLoopContext * theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl];
[del performSelectorOnMainThread:@selector(removeSource :)
withObject:theContext waitUntilDone:YES];
}

注意:应用程序委托registerSource:和removeSource:方法的代码显示在与输入源的客户端协调中。

在运行循环中安装输入源

清单3-7显示了init与addToCurrentRunLoop该方法的RunLoopSource类。该init方法创建CFRunLoopSourceRef必须实际连接到运行循环的不透明类型。它传递RunLoopSource对象本身作为上下文信息,以便回调例程有一个指向对象的指针。输入源的安装不会发生,直到工作线程调用addToCurrentRunLoop方法,此时RunLoopSourceScheduleRoutine调用回调函数。一旦输入源被添加到运行循环中,线程就可以运行它的运行循环来等待它。

清单3-7:安装运行循环源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (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);
}

与输入源的客户端协调

为了使输入源有用,您需要操作它并从另一个线程发出信号。输入源的全部要点是将其关联的线程休眠,直到有事情要做。这个事实需要你的应用程序中的其他线程知道输入源并且有一个与之通信的方法。
将输入源首次安装在运行循环中时,向客户端通知输入源的一种方法是发送注册请求。您可以根据需要为您的输入源注册尽可能多的客户端,或者您可以将其注册到某个中央代理机构,然后将您的输入源发布到感兴趣的客户端。清单3-8显示了由应用程序委托定义的注册方法,并在调用RunLoopSource对象的调度程序函数时调用。该方法接收由RunLoopContext对象提供的RunLoopSource对象并将其添加到其源列表中。此列表还显示了从运行循环中删除输入源时用于注销输入源的例程。

清单3-8:使用应用程序委托注册和删除输入源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (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];
}

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

发信号输入源

将数据交给输入源后,客户端必须发出信号并唤醒其运行循环。信号源让运行循环知道源已准备好被处理。而且因为信号发生时线程可能会睡着,所以你应该总是明确地唤醒运行循环。否则可能会导致处理输入源的延迟。
清单3-9显示fireCommandsOnRunLoop了RunLoopSource对象的方法。当客户端准备好处理添加到缓冲区的命令时,客户端会调用这个方法。

清单3-9:唤醒运行循环
1
2
3
4
5
- (void)fireCommandsOnRunLoop:(CFRunLoopRef)runloop
{
CFRunLoopSourceSignal(runLoopSource);
CFRunLoopWakeUp(runloop);
}

注意: 您不应该SIGHUP通过发送自定义输入源来尝试处理某种或其他类型的进程级别的信号。核心基金会唤醒运行循环的功能不是信号安全的,不应该在应用程序的信号处理程序中使用。有关信号处理程序例程的更多信息,请参阅sigaction手册页。

配置定时器源

要创建定时器源,您只需创建一个定时器对象并将其安排在运行循环中。在Cocoa中,您使用NSTimer该类来创建新的计时器对象,而在Core Foundation中使用CFRunLoopTimerRef不透明类型。在内部,这个NSTimer类只是Core Foundation的一个扩展,它提供了一些便利的功能,比如使用相同的方法创建和调度一个定时器的能力。
在Cocoa中,您可以使用以下任一类方法一次创建和调度一个计时器:

  • scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
  • scheduledTimerWithTimeInterval:invocation:repeats:

这些方法创建定时器,并以默认模式(NSDefaultRunLoopMode)将其添加到当前线程的运行循环中。如果你想通过创建你的NSTimer对象然后用addTimer:forMode:方法将它添加到运行循环中,你也可以手动安排一个计时器NSRunLoop。这两种技术基本上都是一样的,但是可以让你对定时器的配置有不同程度的控制。例如,如果您创建计时器并手动将其添加到运行循环中,则可以使用除默认模式之外的其他模式执行此操作。清单3-10显示了如何使用这两种技术创建定时器。第一个定时器的初始延迟时间为1秒,但之后每0.1秒定时触发一次。第二个定时器在最初的0.2秒延迟后开始发射,然后每0.2秒发射一次。

清单3-10:使用NSTimer创建和安排定时器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
// 创建并安排第一个计时器。
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];
// 创建并安排第二个定时器。
[NSTimer scheduledTimerWithTimeInterval:0.2
target:self
selector:@selector(myDoFireTimer2:)
userInfo:nil
repeats:YES];

清单3-11显示了使用Core Foundation函数配置定时器所需的代码。虽然这个例子没有在上下文结构中传递任何用户定义的信息,但是你可以使用这个结构来传递你定时器所需的任何自定义数据。有关此结构内容的更多信息,请参阅CFRunLoopTimer参考中的说明。

清单3-11:使用Core Foundation创建和计划一个计时器
1
2
3
4
5
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopTimerContext context = {0NULLNULLNULLNULL};
CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault,0.10.300
&myCFTimerCallback,&context);
CFRunLoopAddTimer(runLoop,timer,kCFRunLoopCommonModes);

配置基于端口的输入源

Cocoa和Core Foundation都提供了用于线程间或进程间通信的基于端口的对象。以下部分介绍如何使用几种不同类型的端口设置端口通信。

配置一个NSMachPort对象

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

实现主线程代码

清单3-12显示了启动辅助工作线程的主要线程代码。由于Cocoa框架执行许多配置端口和运行循环的干预步骤,因此该launchThread方法明显短于其Core Foundation等价物(清单3-17); 然而,两者的行为几乎完全相同。一个区别是,不是将本地端口的名称发送给工作线程,而是NSPort直接发送该对象。

清单3-12:主线程启动方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)launchThread
{
NSPort* myPort = [NSMachPort port];
if (myPort)
{
// 这个类处理传入的端口消息。
[myPort setDelegate:self];
// 将端口安装为当前运行循环的输入源。
[[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];
// 分离线程。让工作人员释放端口。
[NSThread detachNewThreadSelector:@selector(LaunchThreadWithPort:)
toTarget:[MyWorkerClass class] withObject:myPort];
}
}

为了在你的线程之间建立一个双向的通信通道,你可能希望工作线程在检入消息中发送自己的本地端口到你的主线程。收到检入信息后,你的主线程就会知道启动第二个线程的过程很顺利,同时也给你一个发送进一步消息的方法。

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

清单3-13:处理Mach端口消息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#define kCheckinMessage 100
// 处理来自工作线程的响应。
- (void)handlePortMessage:(NSPortMessage *)portMessage
{
unsigned int message = [portMessage msgid];
NSPort* distantPort = nil;
if (message == kCheckinMessage)
{
// 获取工作线程的通信端口。
distantPort = [portMessage sendPort];
// 保留并保存工作端口供以后使用。
[self storeDistantPort:distantPort];
}
else
{
// 处理其他消息。
}
}

实现次要线程代码

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

清单3-14显示了用于设置工作线程的代码。为线程创建一个自动释放池之后,该方法创建一个工作者对象来驱动线程执行。工作对象的sendCheckinMessage:方法(如程序清单3-15所示)为工作线程创建一个本地端口,并将一个签入消息发回主线程。

清单3-14:使用Mach端口启动工作线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
+(void)LaunchThreadWithPort:(id)inData
{
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
// 建立这个线程和主线程之间的连接。
NSPort* distantPort = (NSPort*)inData;
MyWorkerClass* workerObj = [[self alloc] init];
[workerObj sendCheckinMessage:distantPort];
[distantPort release];
// 让运行循环处理事情。
do
{
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
beforeDate:[NSDate distantFuture]];
}
while (![workerObj shouldExit]);
[workerObj release];
[pool release];
}

使用时NSMachPort,本地和远程线程可以使用相同的端口对象进行线程之间的单向通信。换句话说,由一个线程创建的本地端口对象成为另一个线程的远程端口对象。
清单3-15显示了辅助线程的签入例程。这个方法建立了自己的本地端口以供将来的通信,然后发送一个签入消息回主线程。该方法使用方法中接收的端口对象LaunchThreadWithPort:作为消息的目标。

清单3-15:使用Mach端口发送登记信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 工作线程签入方法
- (void)sendCheckinMessage:(NSPort*)outPort
{
// 保留并保存远程端口以供将来使用。
[self setRemotePort:outPort];
// 创建并配置工作线程端口。
NSPort* myPort = [NSMachPort port];
[myPort setDelegate:self];
[[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];
// 创建check-in消息。
NSPortMessage* messageObj = [[NSPortMessage alloc] initWithSendPort:outPort
receivePort:myPort components:nil];
if (messageObj)
{
// 完成配置消息并立即发送。
[messageObj setMsgId:setMsgid:kCheckinMessage];
[messageObj sendBeforeDate:[NSDate date]];
}
}

配置一个NSMessagePort对象

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

清单3-16:注册一个消息端口
1
2
3
4
5
6
7
8
9
10
NSPort* localPort = [[NSMessagePort alloc] init];
// 配置对象并将其添加到当前运行循环中。
[localPort setDelegate:self];
[[NSRunLoop currentRunLoop] addPort:localPort forMode:NSDefaultRunLoopMode];
// 使用特定名称注册端口。该名称必须是唯一的。
NSString* localPortName = [NSString stringWithFormat:@"MyPortName"];
[[NSMessagePortNameServer sharedInstance] registerPort:localPort
name:localPortName];

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

本节介绍如何使用Core Foundation在应用程序的主线程和工作线程之间建立双向通信通道。

清单3-17显示了应用程序的主线程调用的启动工作线程的代码。代码的第一件事是建立一个CFMessagePortRef不透明类型来监听来自工作线程的消息。工作者线程需要端口的名称来建立连接,以便将字符串值传递给工作线程的入口点函数。在当前的用户环境中,端口名称通常应该是唯一的; 否则,您可能会遇到冲突。

清单3-17: 将Core Foundation消息端口附加到新线程
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()
{
// 创建一个本地端口来接收响应。
CFStringRef myPortName;
CFMessagePortRef myPort;
CFRunLoopSourceRef rlSource;
CFMessagePortContext context = {0, NULL, NULL, NULL, NULL};
Boolean shouldFreeInfo;
// 使用端口名称创建一个字符串。
myPortName = CFStringCreateWithFormat(NULL, NULL, CFSTR("com.myapp.MainThread"));
// 创建端口
myPort = CFMessagePortCreateLocal(NULL,
myPortName,
&MainThreadResponseHandler,
&context,
&shouldFreeInfo);
if (myPort != NULL)
{
// 该端口已成功创建。
// 现在为它创建一个运行循环源。
rlSource = CFMessagePortCreateRunLoopSource(NULL, myPort, 0);
if (rlSource)
{
// 将源代码添加到当前的运行循环中。
CFRunLoopAddSource(CFRunLoopGetCurrent(), rlSource, kCFRunLoopDefaultMode);
//一旦安装,这些可以被释放。
CFRelease(myPort);
CFRelease(rlSource);
}
}
// 创建线程并继续处理。
MPTaskID taskID;
return(MPCreateTask(&ServerThreadEntryPoint,
(void*)myPortName,
kThreadStackSize,
NULL,
NULL,
NULL,
0,
&taskID));
}

在安装了端口和启动线程的情况下,主线程可以在等待线程检入的情况下继续执行正常的执行。当检入信息到达时,它将被分派到主线程的MainThreadResponseHandler函数中,如清单3-18所示。该函数提取工作线程的端口名称,并为将来的通信创建一个管道。

清单3-18:接收签入消息
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
// 主线程端口消息处理程序
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);
// 您必须按名称获取远程消息端口。
messagePort = CFMessagePortCreateRemote(NULL, (CFStringRef)threadPortName);
if (messagePort)
{
// 保留并保存线程的通信端口以备将来参考。
AddPortToListOfActiveThreads(messagePort);
// 由于端口被前一个函数保留,释放
// 在这里
CFRelease(messagePort);
}
// 清理.
CFRelease(threadPortName);
CFAllocatorDeallocate(NULL, buffer);
}
else
{
// 处理其他消息.
}
return NULL;
}

在配置主线程的情况下,剩下的唯一东西是新创建的工作线程创建自己的端口并进行检入。清单3-19显示了工作线程的入口点函数。该函数提取主线程的端口名称,并使用它来创建远程连接回主线程。然后该函数为自己创建一个本地端口,在该线程的运行循环中安装该端口,并向包含本地端口名称的主线程发送一个签入消息。

清单3-19:设置线程结构
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)
{
// 创建远程端口到主线程。
CFMessagePortRef mainThreadPort;
CFStringRef portName = (CFStringRef)param;
mainThreadPort = CFMessagePortCreateRemote(NULL, portName);
// 释放在param中传递的字符串。
CFRelease(portName);
// 为工作线程创建一个端口。
CFStringRef myPortName = CFStringCreateWithFormat(NULL, NULL, CFSTR("com.MyApp.Thread-%d"), MPCurrentTaskID());
// 将端口存储在此线程的上下文信息中以供日后参考。
CFMessagePortContext context = {0, mainThreadPort, NULL, NULL, NULL};
Boolean shouldFreeInfo;
Boolean shouldAbort = TRUE;
CFMessagePortRef myPort = CFMessagePortCreateLocal(NULL,
myPortName,
&ProcessClientRequest,
&context,
&shouldFreeInfo);
if (shouldFreeInfo)
{
// 不能创建一个本地端口,所以杀死线程。
MPExit(0);
}
CFRunLoopSourceRef rlSource = CFMessagePortCreateRunLoopSource(NULL, myPort, 0);
if (!rlSource)
{
// 不能创建一个本地端口,所以杀死线程。
MPExit(0);
}
// 将源代码添加到当前的运行循环中。
CFRunLoopAddSource(CFRunLoopGetCurrent(), rlSource, kCFRunLoopDefaultMode);
// 一旦安装,这些可以被释放。
CFRelease(myPort);
CFRelease(rlSource);
// 打包端口名称并发送签入消息。
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);
// 清理线程数据结构。
CFRelease(outData);
CFAllocatorDeallocate(NULL, buffer);
// 输入运行循环
CFRunLoopRun();
}

一旦进入运行循环,所有未来发送到线程端口的事件都由ProcessClientRequest函数处理。该函数的实现取决于线程所做的工作类型,在此不显示。