运行循环是与线程相关的基础的一部分。运行循环是一个事件处理循环,用于调度工作并协调事件的接收。运行循环的目的是在有工作要做时保持线程忙,当没有线程时将线程放在睡眠中。
运行循环管理不是完全自动化的。还必须设计线程的代码以在适当的时间启动运行循环并响应传入的事件。Cocoa 和 Core Foundation都提供运行循环对象,以帮助您配置和管理线程的运行循环。您的应用程序不需要显式地创建这些对象;每个线程,包括应用程序的主线程,都有一个关联的运行循环对象。但是,只有子线程需要明确地运行它们的运行循环。应用程序框架自动设置和运行主线程上的运行循环作为应用程序启动过程的一部分。
以下部分提供有关运行循环的更多信息,以及如何为应用程序配置它们。有关运行循环对象的其他信息,请参阅NSRunLoop Class Reference和CFRunLoop Reference.
运行循环听起来就像它的名字一样。这是一个 线程进入运行的事件处理程序以响应传入的事件的循环。你的代码提供了用于实现运行循环的部分的控制语句,换句话说,你的代码提供了while或for循环驱动运行循环。在你的循环里,你使用一个runloop对象“运行”的事件处理代码,接收事件并调用安装程序。
运行循环从两种不同类型的源接收事件。输入源提供异步事件,通常是来自另一线程或来自不同应用程序的消息。定时器源提供同步事件,在预定时间或重复间隔发生。这两种类型的源都使用专用的处理程序来处理事件到达时的事件。
图3-1显示了运行循环的概念结构和各种来源。输入源将异步事件传递给相应的处理程序,并导致runUntilDate: 方法(调用线程关联的NSRunLoop对象)退出。计时器源将事件传递给它们的处理程序,但不会导致运行循环退出。
除了处理输入源之外,run loops还生成有关runloop行为的通知。注册的runloop observers可以接收这些通知并使用它们来对线程进行额外的处理。使用Core Foundation在线程上安装run-loop observers。
以下部分提供了有关运行循环组件及其操作模式的更多信息。它们还描述了在处理事件期间在不同时间生成的通知。
运行循环模式是要监视的输入源和定时器的集合,以及要通知的运行循环观察器的集合。每次运行run循环时,都指定(显式或隐式)特定的“模式”,以便在其中运行。在运行循环的传递过程中,只监视与该模式相关联的源并允许传递它们的事件。(类似地,只有与该模式相关联的观察者才被通知运行循环的进展。)与其他模式相关的源不做任何操作,直到随后使用到的合适的模式。
在代码中,通过名称识别模式。Cocoa 和 Core Foundation都定义了默认模式和几种常用的模式,以及用于指定代码中的这些模式的字符串。您可以通过简单地为模式名指定自定义字符串来定义自定义模式。虽然分配给自定义模式的名称是任意的,但这些模式的内容不是任意的。您必须确保将一个或多个输入源、定时器或运行循环观察器添加到您为它们创建的任何模式中是有用的。
在特定的通过运行循环的过程中,使用模式从无用的sources中筛选出事件。大多数情况下,您希望在系统定义的“默认”模式下运行runloop。模态面板,可能运行在“模态”模式。在这种模式下,只有与模态面板相关的源才会将事件传递给线程。对于子线程,您可以使用自定义模式来防止低优先级源在时间关键操作期间传递事件。
注意:模式基于事件的来源而不是事件的类型来区分。例如,您不使用模式只匹配鼠标下拉事件或仅匹配键盘事件。您可以使用模式来监听不同的端口集,临时暂停计时器,或者以其他方式更改当前监听的sources和observers
表3-1列出了Cocoa 和 Core Foundation定义的标准模式以及使用该模式时的描述。Name列列出了用于在代码中指定模式的实际常数。
输入源异步地向线程传递事件。事件的来源取决于输入源的类型,通常是两种类型之一。基于端口的输入源监听应用程序的Mach端口。自定义输入源监听事件的自定义来源。就运行循环而言,输入源是基于端口的还是自定义的都不重要。系统通常实现两种类型的输入源,它们可以按原样使用。这两个来源之间唯一的区别是它们是如何发出信号的。基于端口的源代码由内核自动发出,并且自定义源必须手动从另一个线程发出信号。
创建输入源时,将其分配给运行循环的一个或多个模式。模式影响在任何给定时刻监视哪些输入源。大多数情况下,在默认模式下运行run循环,但也可以指定自定义模式。如果输入源不在当前监视模式下,则它生成的任何事件都被保持,直到运行循环以正确模式运行。
下面的部分描述了一些输入源。
Cocoa 和 Core Foundation为 使用端口相关对象和函数创建基于端口的输入源提供内置支持。例如,在COCOA中,您根本不需要直接创建输入源。您只需创建一个端口对象,并使用NSPort方法将该端口添加到运行循环中。端口对象为您处理所需输入源的创建和配置。
在Core Foundation,您必须手动创建端口及其运行循环源。在这两种情况下,您都使用与端口不透明类型(CFMachPortRef, CFMessagePortRef, or CFSocketRef)相关的函数来创建适当的对象。
有关如何设置和配置基于自定义端口的源的示例,请参见配置基于端口的输入源。
若要创建自定义输入源,必须使用与 Core Foundation中的CFRunLoopSourceRef不透明类型相关联的函数。您可以使用多个回调函数配置自定义输入源。 Core Foundation在不同点调用这些函数来配置输入源,处理任何传入事件,并在从runloop移除时销毁。
除了定义事件到达时自定义源的行为外,还必须定义事件传递机制。源的这一部分运行在单独的线程上,负责向输入源提供其数据,并在数据准备处理时发出信号。事件传递机制取决于您,但不必过于复杂。
有关如何创建自定义输入源的示例,请参见定义自定义输入源。有关自定义输入源的参考信息,请参阅CFRunLoopSource Reference.
除了基于端口的源外,COCOA还定义了一个自定义输入源,允许您在任何线程上执行selector。与基于端口的源一样,perform selector 要求在目标线程上被序列化,减轻了在一个线程上运行多个方法时可能出现的许多同步问题。与基于端口的源不同,a perform selector source 在执行selector之后将其自身从运行循环中移除。
注意:在OS X v10.5之前,执行选择器源主要用于向主线程发送消息,但是在OS X v10.5和以后以及在IOS中,您可以使用它们来向任何线程发送消息。
当在另一个线程上执行selector时,目标线程必须有一个活动的运行循环。对于创建的线程,这意味着等待,直到代码显式启动运行循环。但是,由于主线程启动了自己的运行循环,一旦应用程序调用Apple的applicationDidFinishLaunching:的代理方法,就可以开始在该线程上发出调用。运行循环每次通过循环处理所有队列的perform selector调用,而不是在每个循环迭代过程中处理一个。
表3-2列出了可用于在其他线程上使用perform selectors的NSObject定义的方法。因为这些方法是在NSObject上声明的,所以您可以从任何可以访问Objective-C对象(包括POSIX线程)的线程中使用它们。这些方法实际上并不创建一个新的线程来执行选择器。
Table 3-2 Performing selectors on other threads
有关这些方法中的每种方法的详细信息,请参见NSObject Class Reference.
定时器源在未来的预设时间同步地向线程传递事件。定时器是线程通知自己做某事的一种方式。例如,搜索字段可以使用计时器在用户连续键击之间经过一定时间后启动自动搜索。这种延迟时间的使用使用户有机会在开始搜索之前尽可能多地键入期望的搜索字符串。
虽然它生成基于时间的通知,但定时器不是实时机制。与输入源一样,定时器与运行循环的特定模式相关联。如果计时器不在当前由运行循环监听的模式中,则直到在计时器的支持模式之一运行运行循环时才启动。类似地,如果一个计时器在运行循环处于执行一个处理程序的中间时触发,则定时器等到运行循环的下一次调用它的处理程序。如果运行循环根本没有运行,定时器就不会触发。
可以配置定时器只生成一次或多次生成事件。重复计时器根据预定的触发时间自动重新安排,而不是实际的触发时间。例如,如果一个计时器被安排在一个特定的时间和之后的每5秒触发,即使实际的触发时间延迟,预定的触发时间也总是落在原来的5秒时间间隔上。如果触发时间被延迟太多以致错过一个或多个预定的触发时间,则在错过的时间段只触发一次定时器。在错过的时间触发之后,计时器重新安排下一个预定的触发时间。
有关配置计时器源的详细信息,请 Configuring Timer Sources。有关参考信息,请参阅 NSTimer Class Reference 或CFRunLoopTimer Reference
与source相反,当发生适当的异步或同步事件时, run loop observers在运行循环本身执行期间在特殊位置触发。您可以使用运行循环observers来准备线程去处理给定事件,或者在线程进入睡眠前准备线程。您可以将运行循环观察器与运行循环中的下列事件相关联:
·运行循环的入口。
·当运行循环即将处理计时器时。
·当运行循环即将处理输入源时。
·当运行回路即将进入睡眠状态时。
·当运行循环已经唤醒,但在它处理了唤醒它的事件之前。
·从运行循环退出。
可以使用Core Foundation向应用程序添加运行循环observers。若要创建运行循环observers,请创建CFRunLoopObserverRef不透明类型的新实例。此类型跟踪您的自定义回调函数及其感兴趣的活动。
类似于定时器,运行循环observers可以使用一次或多次。一个一次命中的observer在运行后从run loop中移除自己,而重复的观察者仍然附属。您指定一个observer在创建它时是否运行一次或多次。
有关如何创建run-loop observer的示例,请参见 Configuring the Run Loop。有关参考信息,请参见CFRunLoopObserver Reference.
每次运行它时,线程的运行循环处理挂起的事件,并为任何附加的观察者生成通知。它这样做的顺序是非常具体的,如下:
每次运行runloop时,线程的runloop会自动处理挂起的事件(之前未处理事件),并通知观察者。具体的顺序如下:
1.通知观察者runloop已经启动。
2.通知观察者任何即将要开始的定时器。
3.通知观察者任何非基于端口的输入源。
4.启动任何准备好的非端口的输入源。
5.如果基于端口的输入源已准备好并处于等待状态,则立即处理该事件。转到步骤9。
6.通知观察者线程即将进入休眠。
7.将线程置于休眠状态,直到发生下列事件之一:
·某一事件到达基于端口的源。
·定时器启动。
·runloop的超时值已过期。
·runloop被显式唤醒。
8.通知观察员线程刚刚醒来。
9.处理挂起的事件。
·如果用户定义的定时器被触发,则处理定时器事件并重新启动runloo。转到步骤2。
·如果输入源启动,则传递相应的消息事件。
·如果runloop被明确唤醒但尚未超时,则重新启动runloop。转到步骤2。
10.通知观察者runloop已退出。
因为定时器和输入源的观察者通知在这些事件实际发生之前被传递,所以通知的时间和实际事件的时间之间可能存在间隙。如果这些事件之间的定时是决定性的,则可以使用休眠通知和休眠唤醒来帮助您将实际事件之间的时序关联起来。
因为在运行循环时传递定时器和其他周期性事件,绕过该循环会中断这些事件的传递。每当您通过输入循环并重复请求来自应用程序的事件来实现鼠标跟踪例程时,就会出现此行为的典型示例。因为您的代码正直接捕获事件,而不是让应用程序正常地发送这些事件,所以活动定时器将无法在您的鼠标跟踪例程退出后向应用程序返回控制。
运行循环可以使用runloop对象显式唤醒。其他事件也可能导致运行循环被唤醒。例如,添加另一个基于非端口的输入源唤醒运行循环,以便可以立即处理输入源,而不是等到发生其他事件。
只有在创建子线程时,才需要显式运行一个runloop。应用程序的主线程的运行循环是基础关键的部分。因此,应用程序框架提供了运行主应用程序循环并自动启动该循环的代码。
UIApplication in iOS (or NSApplication in OS X)里的run 方法启动应用程序的主循环作为正常启动序列的一部分。如果使用Xcode模板项目来创建应用程序,则不必显式调用这些例程。
对于子线程,您需要决定是否需要运行循环,如果是,请自行配置和启动。在所有情况下,不需要启动线程的运行循环。例如,如果使用线程执行一些长时间运行的和预定的任务,则可能避免启动运行循环。runloop用于希望与线程进行更多交互性的情况。例如,如果您计划执行下列操作,则需要启动运行循环:
·使用ports或custom input sources与其他线程通信
·在该线程使用timer
·使用任一performSelector…方法
·让线程循环执行周期性任务
如果选择使用运行循环,则配置和安装很简单。与所有线程编程一样,在适当的情况下,您应该有退出子线程的计划。结束线程时,最好是让线程退出,而不是强制终止。在运行循环对象中描述了如何配置和退出运行循环的信息,请参考Using Run Loop Objects。
runloop对象提供主接口,用于向运行循环添加输入源、定时器和runloop observers,然后运行它。每个线程都有一个与它相关联的单个运行循环对象。在COCOA中,此对象是NSRunLoop类的实例。在低级应用程序中,它是指向CFRunLoopRef不透明类型的指针。
若要获取当前线程的运行循环,请使用下列之一:
·使用NSRunLoop的currentRunLoop 类方法重新获取NSRunLoop对象
·使用CFRunLoopGetCurrent()函数
虽然它们不是免费桥接类型,但是当需要时,可以从NSRunLoop对象获得CFRunLoopRef不透明类型。NSRunLoop类定义了一个getCFRunLoop方法,它返回一个可以传递给 Core Foundation例程的CFRunLoopRef类型。因为两个对象都引用相同的运行循环,所以可以根据需要将调用与NSRunLoop对象和CFRunLooRfCFRunLoopRef类型混合。
在运行子线程上的运行循环之前,必须向其添加至少一个输入源或定时器。如果运行循环没有任何要监视的源,则当您尝试运行它时,它立即退出。有关如何向运行循环添加源代码的示例,请参见Configuring Run Loop Sources。
除了安装源程序外,还可以安装运行循环observer,并使用它们来检测运行循环的不同执行阶段。要安装运行循环观察器,您需要创建一个CFRunLoopObserverRef不透明类型,并使用CFRunLoopAddObserver观测器函数将其添加到运行循环中。运行循环观察器必须使用Core Foundation,甚至Cocoa applications。
清单3-1显示了一个线程,它将运行循环观察器连接到运行循环的主例程。该示例的目的是向您展示如何创建运行循环观察器,因此代码只需设置运行循环观察器来监视所有运行循环活动。基本处理程序(未示出)在处理定时器请求时简单地记录运行循环活动。
- (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);
}
在为long-lived线程配置运行循环时,最好添加至少一个输入源来接收消息。虽然只需添加一个定时器就可以进入运行循环,但一旦定时器触发,它通常会失效,这将导致运行循环退出。附加一个重复的计时器可以使运行循环在更长的时间内运行,但涉及到定时启动计时器唤醒线程,这实际上是另一种轮询形式。相比之下,输入源等待事件发生,直到线程保持睡眠。
启动运行循环仅适用于应用程序中的子线程。运行循环必须至少有一个输入源或计时器来监控。如果没有连接,则运行循环立即退出。
启动Run循环的方法有以下几种:
·无条件地
·设定时间限制
·在特定模式下
无条件地进入运行循环是最简单的选择,但也是最不可取的。无条件地运行run循环将线程放入永久循环,这对运行循环本身的控制非常少。您可以添加和移除输入源和定时器,但是停止运行循环的唯一方法是杀死它。也没有办法在自定义模式下运行runloop。
与其无条件地运行run循环,最好使用超时值运行run循环。当使用超时值时,运行循环运行直到事件到达或分配的时间过期。如果事件到达,则将事件发送到处理程序进行处理,然后运行循环退出。然后,您的代码可以重新启动运行循环来处理下一个事件。如果分配的时间到期,您可以简单地重新启动运行循环或使用时间做任何需要的housekeeping。
除了超时值之外,还可以使用特定模式运行运行循环。模式和超时值不是互斥的,并且在启动循环时都可以使用。模式限制将事件传递到运行循环的源类型,并在RunLoop Modes中更详细地描述这些类型。
清单3-2显示了线程的主入口例程的骨架版本。这个例子的关键部分显示了运行循环的基本结构。本质上,您将输入源和定时器添加到运行循环中,然后重复调用其中一个例程来启动运行循环。每次运行循环例程返回时,检查是否出现了可能退出线程的条件。该示例使用 Core Foundation运行循环例程,以便检查返回结果并确定运行循环退出的原因。如果您使用COCOA并且不需要检查返回值,您还可以使用NSRunLoop类的方法以类似的方式运行Run循环。(对于调用NSRunLoop类方法的运行循环的示例,请参见清单3-14)。
- (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.
}
it is possible to run a run loop recursively. In other words, you can call CFRunLoopRun, CFRunLoopRunInMode, or any of the NSRunLoop methods for starting the run loop from within the handler routine of an input source or timer. When doing so, you can use any mode you want to run the nested run loop, including the mode in use by the outer run loop.
可以递归地运行一个运行循环。换句话说,您可以调用CFRunLoopRun、CFRunLoopRunInMode或任何NSRunLoop方法,用于从输入源或定时器的处理程序中启动运行循环。这样做时,您可以使用任何想要运行嵌套运行循环的模式,包括由外部运行循环使用的模式。
在处理一个事件之前,有两种方法使运行循环退出:
·用超时值配置runloop。
·告诉运行循环停止。
使用超时值当然是首选,如果您能够管理它。指定超时值允许运行循环完成所有正常处理,包括在退出之前向运行循环observer传递通知。
用CFRunLoopStop函数显式地停止运行循环会产生类似于超时的结果。运行循环发出任何剩余的运行循环通知,然后退出。不同之处在于,可以在无条件启动的运行循环上使用此技术。
虽然删除运行循环的输入源和定时器也可能导致运行循环退出,但这不是停止运行循环的可靠方法。一些系统例程将输入源添加到运行循环以处理需要的事件。因为您的代码可能不知道这些输入源,所以无法删除它们,这将阻止运行循环退出。
线程安全性取决于使用哪些API来操纵运行循环。Core Foundation的功能一般是线程安全的,可以从任何线程调用。但是,如果您正在执行更改运行循环配置的操作,那么从可能拥有运行循环的线程中仍然可以很好地实现这一操作。
Cocoa NSRunLoop类不是作为其Core Foundation对应的固有线程安全的。如果您使用NSRunLoop类来修改运行循环,则应该仅从拥有该运行循环的同一线程执行此操作。将输入源或定时器添加到属于不同线程的运行循环可能会导致代码崩溃或以意外的方式运行。
Defining a Custom Input Source 定义自定义输入源
创建自定义输入源涉及定义以下内容:
·您希望输入源进行处理的信息。
·让客户端知道如何联系输入源的调度器例程。
·处理任何客户端发送的请求的处理程序。
·使输入源无效的取消程序。
因为您创建自定义输入源来处理自定义信息,所以实际配置被设计为灵活的。调度器、处理程序和取消例程是您几乎总是需要自定义输入源的关键例程。然而,大多数输入源行为都是在那些处理程序之外发生的。例如,由您来定义将数据传递到输入源并处理输入源与其他线程通信的机制。
图3-2显示了自定义输入源的示例配置。在这个示例中,应用程序的主线程维护对输入源的引用、该输入源的自定义命令缓冲区和安装输入源的运行循环。当主线程有一个任务要切换到工作线程时,它将命令发送到命令缓冲区,以及工作线程启动任务所需的任何信息。(因为工作线程的主线程和输入源都有对命令缓冲区的访问,所以访问必须同步)。一旦命令被发布,主线程就发出输入源信号并唤醒工作者线程的运行循环。在接收到唤醒命令后,运行循环调用输入源的处理程序,该处理程序处理命令缓冲区中找到的命令。
下面的部分将解释从前面的图中自定义输入源的实现,并显示您需要实现的关键代码。
定义自定义输入源需要使用Core Foundation程序来配置运行循环源并将其添加到运行循环。虽然基本的处理程序是基于C的函数,但这并不妨碍您编写这些函数的包装器,并使用Objtovi-C或C++来实现代码的主体。
图3-2中介绍的输入源使用Objective-C对象来管理命令缓冲区,并与运行循环进行协调。清单3-3显示了这个对象的定义。RunLoopSource对象管理命令缓冲区,并使用该缓冲区接收来自其他线程的消息。这个列表还显示了RunLoopContext对象的定义,它实际上是一个容器对象,用于传递RunLoopContext对象和运行循环引用到应用程序的主线程。
Listing 3-3 The custom input source object definition
@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 代码管理输入源的自定义数据,但是将输入源连接到运行循环需要基于C的回调函数。当您实际上将运行循环源附加到运行循环时,调用这些函数中的第一个函数,如清单3-4所示。因为这个输入源只有一个客户机(主线程),所以它使用调度器函数发送消息,用该线程上的应用委托来注册自己。当委托人想要与输入源通信时,它使用RunLoopContext上下文对象中的信息来实现。
Listing 3-4 Scheduling a run loop source
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];
}
最重要的回调例程之一是用于在输入信号源时处理自定义数据的例程。清单3-5显示了与RunLoopSource对象关联的执行回调例程。此函数简单地将请求转发到sourceFired方法,然后处理命令缓冲区中存在的任何命令。
Listing 3-5 Performing work in the input source
void RunLoopSourcePerformRoutine (void *info)
{
RunLoopSource* obj = (RunLoopSource*)info;
[obj sourceFired];
}
如果您曾经使用CFRunLoopSourceInvalidate函数从运行循环中删除输入源,系统将调用输入源的取消例程。您可以使用此例程通知客户端,您的输入源不再有效,并且应该删除对其的任何引用。清单3-6显示了RunLoopSource对象注册的取消回调例程。此函数将另一个RunLoopContext对象发送给应用程序delegate,但这次调用delegate删除对运行循环源的引用。
Listing 3-6 Invalidating an input source
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];
}
注意:应用delegate的registerSource:方法 和 removeSource: 方法显示在与输入源的客户端通信中。
Installing the Input Source on the Run Loop 在运行循环上安装输入源
在运行循环上安装输入源
清单3-7显示了RunLoopSource类的init和addToCurrentRunLoop方法。init方法创建必须实际添加到运行循环的CFRunLoopSourceRef不透明类型。它将RunLoopSource本身作为上下文信息,以便回调例程具有指向该对象的指针。直到工作线程调用addToCurrentRunLoop方法,才会调用输入源的安装,此时调用RunLoopSourceScheduleRoutine回调函数。一旦输入源被添加到运行循环中,线程就可以运行它的运行循环来等待它。
Listing 3-7 Installing the run loop source
- (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);
}
Coordinating with Clients of the Input Source 与客户的输入源协调
为了使您的输入源有用,您需要操作它,并从另一个线程发出信号。输入源的全部要点是把它的关联线程放在睡眠中,直到有事情要做。这一事实需要应用程序中的其他线程知道输入源并有一种与之通信的方式。
通知客户端有关输入源的一种方法是在输入源首次安装在其运行循环时发出注册请求。您可以给尽可能多的客户端注册您的输入源,或您可以简单地注册它与一些中间代理,然后把您的输入源声明给感兴趣的客户端。清单3-8展示了应用程序委托定义的注册方法,并在调用RunLoopSource对象的调度器函数时调用。此方法接收RunLoopSource对象提供的RunLoopContext上下文对象,并将其添加到源列表中。此列表还显示了在从运行循环中移除输入源时取消注册源的例程。
Listing 3-8 Registering and removing an input source with the application delegate
- (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所示。
Signaling the Input Source 信号输入源
在将数据交给输入源之后,客户端必须发出信号并唤醒它的运行循环。信号消息源使运行循环知道源已准备好进行处理。由于线程在信号发生时可能处于休眠状态,因此应该始终明确地唤醒运行循环。如果不这样做,可能会导致处理输入源的延迟。
清单3-9显示了RunLoopSource对象的fireCommandsOnRunLoop方法。当客户端准备好处理源代码添加到缓冲区的命令时,客户端调用该方法。
Listing 3-9 Waking up the run loop
- (void)fireCommandsOnRunLoop:(CFRunLoopRef)runloop
{
CFRunLoopSourceSignal(runLoopSource);
CFRunLoopWakeUp(runloop);
}
注意:您不应该尝试通过发送自定义输入源来处理一个SIGHUP或其他类型的进程级信号。唤醒运行循环的 Core Foundation函数不是信号安全的,不应该在应用程序的信号处理程序例程中使用。有关信号处理程序例程的详细信息,请参阅sigaction man page。
###Configuring Timer Sources 配置定时器源
要创建一个定时器源,你所要做的就是创建一个定时器对象并在运行循环上调度它。在COCOA中,使用NSTimer类创建新的定时器对象,而在Core Foundation中使用CFRunLoopTimerRef不透明类型。在内部,NSTimer类仅仅是Core Foundation的扩展,它提供了一些方便的特性,例如使用相同的方法创建和调度计时器的能力。
在COCOA中,可以使用这些类方法中的任何一种同时创建和调度计时器:
scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
scheduledTimerWithTimeInterval:invocation:repeats:
这些方法创建计时器并将其添加到默认线程模式下的当前线程的运行循环(NSDefaultRunLoopMode)。如果您想创建NSTimer对象,然后使用addTimer:forMode: 方法将其添加到运行循环,您还可以手动调度计时器。这两种技术基本上都是一样的,但是对定时器的配置给予不同的控制。例如,如果您创建了计时器并手动将其添加到运行循环,则可以使用默认模式以外的模式。清单3-10展示了如何使用这两种技术创建计时器。第一个定时器具有1秒的初始延迟,但之后每隔0.1秒定时触发。第二个定时器在最初的0.2秒延迟之后开始触发,然后在每0.2秒之后触发。
Listing 3-10 Creating and scheduling timers using NSTimer
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];
清单3-11显示了使用Core Foundation函数配置计时器所需的代码。虽然此示例不传递上下文结构中的任何用户定义的信息,但可以使用此结构来传递定时器所需的任何自定义数据。有关此结构的内容的更多信息,请参见其在CFRunLoopTimer Reference中的描述。
清单3-11利用Core Foundation创建和调度计时器
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 and Core Foundation都提供基于端口的对象,用于在线程之间或进程之间通信。下面的部分将介绍如何使用几种不同类型的端口来建立端口通信。
要与NSMachPort端口对象建立本地连接,您需要创建端口对象并将其添加到主线程的运行循环中。在启动子线程时,将同一对象传递到线程的入口点函数。子线程可以使用同一个对象将消息发送回主线程。
实现主线程代码
清单3-12显示了启动子工作线程的主要线程代码。因为COCOA框架执行许多用于配置端口和运行循环的中间步骤,所以启动线程方法明显短于其Core Foundation等价方法(清单3-17);然而,两者的行为几乎是相同的。一个不同之处在于,不是将本地端口的名称发送给工作线程,而是直接发送NSPort对象。
Listing 3-12 Main thread launch method
- (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];
}
}
为了在线程之间建立双向通信通道,您可能希望工作线程在check-in message中将自己的本地端口发送到主线程。接收check-in message可以让主线程知道在启动子线程时一切顺利,并且还提供了一种向该线程发送进一步消息的方法。
清单3-13显示了主线程的handlePortMessage:方法。当数据到达线程自己的本地端口时,调用此方法。当check-in message到达时,该方法直接从端口消息检索子线程的端口,并将其保存以供以后使用。
Listing 3-13 Handling Mach port messages
#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.
}
}
Implementing the Secondary Thread Code 实现子线程代码
对于子工作线程,必须配置线程并使用指定的端口将信息传递回主线程。
清单3-14显示了建立工作线程的代码。在为线程创建了一个autorelease池之后,该方法创建一个worker对象来驱动线程执行。work对象的sendCheckinMessage:方法(如清单3-15所示)为工作线程创建一个本地端口,并将check-in message发送回主线程。
清单3-14使用Mach端口启动工作线程
Listing 3-14 Launching the worker thread using Mach ports
+(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时,本地线程和远程线程可以使用同一端口对象进行线程间的单向通信。换句话说,由一个线程创建的本地端口对象成为另一个线程的远程端口对象。
清单3-15显示了子线程的 check-in例程。该方法设置自己的本地端口,用于将来的通信,然后将 check-in message发送回主线程。该方法使用在LaunchThreadWithPort端口:方法中接收的端口对象作为消息的目标。
Listing 3-15 Sending the check-in message using Mach ports
// 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中实现这一点需要用特定的名称注册本地端口,然后将该名称传递给远程线程,以便它可以获得适当的端口对象进行通信。清单3-16显示了您希望使用消息端口的情况下的端口创建和注册过程。
Listing 3-16 Registering a message port
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];
Configuring a Port-Based Input Source in Core Foundation
本节演示如何使用Core Foundation在应用程序的主线程和工作线程之间建立双向通信通道。
清单3-17显示了应用程序的主线程调用的启动工作者线程的代码。代码所做的第一件事是建立一个CFMessagePortRef不透明类型,以侦听来自工作线程的消息。工作线程需要端口的名称来进行连接,从而将字符串值传递给辅助线程的入口点函数。端口名称通常在当前用户上下文中是唯一的;否则,您可能会遇到冲突。
Listing 3-17 Attaching a Core Foundation message port to a new thread
#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));
}
随着端口的安装和线程的启动,主线程可以在等待线程check-in时继续正常执行。当check-in message到达时,它被发送到主线程的MainThreadResponseHandler函数,如清单3-18所示。此函数提取工作线程的端口名称,并创建用于未来通信的管道。
Listing 3-18 Receiving the checkin message
#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;
}
在配置主线程时,剩下的就是新创建的工作线程创建自己的端口和check-in。清单3-19显示了工作线程的入口点函数。函数提取主线程的端口名称,并使用它创建一个远程连接返回主线程。然后,函数为自己创建本地端口,在线程的运行循环上安装端口,并向主线程发送check-in message,其中包含本地端口名称。
Listing 3-19 Setting up the thread structures
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();
}
一旦进入运行循环,发送到线程端口的所有未来事件都由ProcessClientRequest请求函数处理。该函数的实现取决于线程所做的工作类型,这里没有显示。