翻译来源: RunLoops
Run Loops
RunLoops是与线程紧密相关的基础架构的一部分,简称运行循环。RunLoop是一个事件处理循环,用于安排工作并协调接收到的事件。RunLoop的目的是在有任务的时候线程处于繁忙状态(thread busy),并在没有任务的时候线程处于休眠状态(thread sleep)。
RunLoop管理不是完全自动的。我们仍然需要设计线程的代码以便在适当的时间启动RunLoop并响应传入的事件。Cocoa和Core Foundation都提供了RunLoop对象,以帮助我们配置和管理线程的RunLoop。我们不需要明确的创建RunLoop对象;每一个线程(包含主线程在内的)都有一个与之关联的RunLoop对象。但是,只有次线程需要显式的运行RunLoop。作为应用程序启动的一部分,应用程序框架自动设置并在主线程上运行RunLoop。
下面将提供有关RunLoop的更多信息以及如何为应用程序配置它们。有关RunLoop对象的更多信息,请参考 NSRunLoop Class Reference and CFRunLoop Reference.
一、Run Loop的解析
RunLoop就像它的名字一样是一个运行循环。在这个检测循环内,使用与之绑定的线程来运行事件处理程序以响应传入的事件。我们的代码用于实现RunLoop的实际循环部分的控制语句,换句话说,这部分代码将驱动RunLoop的while或者for循环。在循环中,使用RunLoop对象来“运行”接收事件并调用已安装处理程序的代码处理事件。
Run Loop接收两种不同类型源的事件。输入源(input sources)传递异步事件,通常是来自另一个线程或者来自不同的应用程序的消息。计时器源(Timer sources)提供发生在计划时间或重复间隔的同步事件。这两种类型的源都使用特定于应用程序的处理程序来处理事件到达时的状态。
图(1-1)显示了RunLoop和各种来源的概念结构。输入源(input sources)将异步事件传递给相应的处理程序,并导致runUntilDate:方法(在线程关联的RunLoop对象上调用)退出。计时器源(Timer source)将事件传递到其处理程序例程,但不会导致RunLoop退出。
除了处理输入源(input sources)之外, Run Loop还会生成有关RunLoop行为的通知。已注册的Run Loop观察器可以接收这些通知并使用它们对线程进行附加处理。我们可以使用Core Foundtion 在线程上安装Run Loop观察器。
下面的部分提供了关于RunLoop的组件和其运行模式的更多信息。它们还描述了在处理事件期间在不同时间生成的通知。
1.1Run Loop模式
RunLoop模式是要监视的输入源和计时器的集合,以及要通知的RunLoop观察器的集合。每次运行RunLoop时,都需要(显式或隐式的)指定要运行的特定"模式"。在运行循环的过程中,只监视与该模式关联的源并允许其发送事件。(同样,只有与该模式关联的观察器才会收到RunLoop进程的通知。)与其他模式相关的源保存到任何新事件,直到随后以适当的模式通过循环为止。
在代码中,我们可以通过名字来识别模式。Cocoa和Core Foundation都定义了一个默认模式和几种常用模式,以及用于在代码中指定这些模式的字符串。我们可以通过为模式名称指定自定义字符串定义自定义模式。虽然分配给自定义模式的名称是任意的,但这些模式的内容不是。因此必须确保将一个或者多个输入源、计时器或者RunLoop观察器添加到与我们创建的模式中,以便它们有用。
在特定的RunLoop中使用模式可以过滤掉不需要的源中的事件。大多数情况下,我们需要在系统定义的“默认”模式下运行RunLoop。但是,模态面板可能会以“模态”模式运行。在此模式下,只有与模态面板相关的源才会将事件传递给线程。对于辅助线程,也可以使用自定义模式来防止低优先级的源在时间关键型操作期间传递事件。
注意:模式基于事件的源进行区分,而不是事件的类型。例如,我们不会使用模式仅匹配鼠标点击(mousedown)事件或仅匹配键盘事件。我们也可以使用模式监听一组不同的端口,暂时暂停计时器,或者更改当前正在监视的源代码和RunLoop观察器。
下面列出了Cocoa和Core Foundation定义的标准模式以及何时使用该模式的描述。
模式1
model : Default
Name:NSDefaultRunLoopModel(Cocoa) kCFRunLoopDefaultMode(Core Foundation)
Description : 默认模式是用于大多数操作的模式。 大多数情况下, 我们应该使用此模式来启动RunLoop并配置输入源。
模式2:
model : Connection
Name: NSConnectionReplyModel(Cocoa)
Description : Cocoa将此模式与NSConnection对象一起使用来监听响应。我们应该很少需要自己使用这种模式。
模式3:
model : Modal
Name:NSModelPanelRunLoopModels(Cocoa)
Description : Cocoa使用该模式识别用于模态面板的事件
模式4:
model : Event tracking
Name: NSEventTrackingRunLoopModel(Cocoa)
Description : Cocoa使用该模式来限制鼠标拖动循环和其他类型的用户界面跟踪循环期间的传入的事件。
模式5:
model : Common modes
Name:NSRunLoopCommonModes(Cocoa)kCFRunLoopCommonModes(Core Foundation)
Description : 这是一组可配置的常用模式组。将输入源与此模式相关联也会将其与组中的每一个模式相关联。对于Cocoa应用程序,默认情况下,此集合包含默认、模式和事件跟踪模式。Core Foundation最初只包含默认模式。我们也可以使用CFRunLoopAddCommonMode函数将自定义模式添加到集合。
1.2输入源
输入源以异步的方式向您的线程传递事件。事件的来源取决于输入源的类型,它通常是两类中的一类。基于端口的输入源监视你的应用程序的Mach端口。自定义输入源监视自定义事件源。就RunLoop而言,输入源是基于端口的还是自定义的应该没有关系。系统通常实现两种类型的输入源,我们可以按照原样使用它们。两个来源之间的唯一区别是他们如何发出信号。基于端口的源由内核自动发送信号,自定义源必须从另一个线程手动发送信号。
创建输入源时,可以将其分配给RunLoop的一个或多个模式。模式会影响在特定的时刻监视哪些输入源。大多数情况下,在默认模式下运行RunLoop,但也可以指定自定义模式。如果输入源不处于当前监控的模式,则会生成其生成的所有事件,直到RunLoop以正确的模式运行。
下面是各个输入源的介绍。
1.2.1.基于端口的源
Cocoa和Core Foundation为使用与端口相关的对象和函数创建基于端口的输入源提供了内置的支持。例如,在Cocoa中,我们不需要直接创建输入源。而是只需要创建一个端口对象并使用NSPort的方法将该端口添加到RunLoop。port对象为我们处理所需要输入源的创建和配置。
在Core Foundation中,我们必须手动创建端口及其RunLoop源。在这两种情况下,都使用与端口不透明类型(CFMachPortRef,CFMessagePortRef或CFSocketRef)相关的函数来创建适当的对象。
有关如何设置和配置自定义的端口源的示例,请参阅3.3配置基于端口的输入源。
1.2.2.自定义输入源
要创建自定义的输入源,我们必须使用与Core Foundation中的 CFRunLoopSourceRef 不透明类型关联的函数。我们可以使用多个回调函数来配置自定义的输入源。Core Foundation在不同的点调用这些函数来配置源代码,处理所有的传入事件,并在源代码从RunLoop中移除时删除源代码。
除了在事件到达时定义自定义源的行为之外,还必须定义事件传递机制。这部分源代码在单独的线程上运行,负责为输入源提供数据,并在数据准备好处理时用信号通知它。事件传递机制取决于我们,但不必过于复杂。
有关如何创建自定义输入源的示例,请参阅下文定义自定义的输入源。有关自定义输入源的参考信息,请参阅CFRunLoopSource参考。
1.2.3.Cocoa执行选择器源
除了基于端口的源代码之外,Cocoa还定义了一个自定义输入源,允许我们在任何线程执行选择器。与基于端口的源一样,执行选择器请求在目标线程上被序列化,从而减轻了在一个线程上运行多个方法时可能发生的许多同步问题。与基于端口的源不同,执行选择器源在执行其选择器后从RunLoop中移除。
注意:在OS X v10.5之前,执行选择器源主要用于将消息发送到主线程,但在OS X v10.5及更高版本和iOS中,可以使用它们向任何线程发送消息。
在另一个线程上执行选择器时,目标线程必须具有活动的RunLoop。对于我们创建的线程,这意味着等待我们的代码明确的启动RunLoop。但是,因为主线程的RunLoop是在应用程序调用应用程序的委托的applicationDidFinishLaunching:方法时自己启动的,因此在该方法调用之后就可以开始在主线程上发出调用。RunLoop每次通过循环处理所有排队的执行选择器调用,而不是在每次循环迭代期间处理一个。
下面列出了NSObject上定义的方法,可用于在其他线程上执行选择器。因为这些方法是在NSObject上声明的,所以我们可以在任何有权访问NSObject对象的线程中使用它们,包括POSIX线程。这些方法实际上不会创建一个新的线程来执行选择器。
在其他线程上执行选择器
方法1.
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
以上方法在该线程的下一个运行循环中执行应用程序主线程上的指定选择器。这些方法使我们可以选择阻止当前线程,直到执行选择器。
方法2.
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
以上方法在拥有NSThread对象的任何线程上执行指定的选择器。这些方法可以选择是否阻止当前线程,直到执行选择器。
方法3.
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
以上方法在下一个运行循环周期和可选的延迟周期之后,在当前线程上执行指定的选择器。由于它等待下一个RunLoop执行选择器,所以这些方法会从当前正在执行的代码中提供一个微型的自动延迟。多个排队选择器按照他们排队的顺序依次执行。
方法4.
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:
以上方法允许我们使用performSelector:withObject:afterDelay:或performSelector:withObject:afterDelay:inModes:方法取消发送到当前线程的消息。
1.2.4:定时器源
定时器源在未来的预设时间将事件同步传递给线程。定时器源是线程通知自己做某事的一种方式。例如,搜索字段可以使用定时器在用户的连续击键之间经过一段之间后启动自动搜索。使用此延迟时间使用户有机会再开始搜索之前尽可能多地输入所需的搜索字符串。
虽然它生成基于时间的通知,但计时器不是实时机制。与输入源一样,定时器与RunLoop的特定模式相关联。如果一个定时器不处于当前由RunLoop监视的模式,那么只有在定时器的一种受支持模式下运行RunLoop时才会触发定时器。同样,如果运行循环处于处理程序历程的中间时触发定时器,则定时器将等待下一次通过RunLoop来调用其处理程序例程。如果RunLoop根本没有运行,则定时器不会启动。
我们可以将定时器配置为仅生成一次或重复生成事件。重复计时器会根据预定的执行时间自动重新安排时间,而不是实际的执行时间。例如,如果定时器计划在特定的执行时间以及之后每隔5秒触发一次,则即使实际触发时间延迟,计划的触发时间也会始终以最初的5秒为间隔。如果开始时间延迟太多以至于未能到达一个或者多个预定的执行时间,则计时器在错过的时间段内仅被执行一次。在错过的执行时间后,定时器重新安排下一个预设的执行时间。
有关配置定时器源的更多信息,请参阅3.2配置定时器源。有关参考信息,请参阅NSTimer类参考或CFRunLoopTimer参考。
1.2.5:RunLoop观察器
与发生适当异步事件或同步事件时触发的源相比,RunLoop观察器在RunLoop本身的执行过程中会在特定位置触发。我们可以使用RunLoop观察器来准备线程以及处理给定的事件,或者在线程进入休眠之前准备线程。我们可以将RunLoop观察程序与RunLoop中的以下事件相关联:
1.进入RunLoop。
2.RunLoop即将处理计时器。
3.RunLoop即将处理输入源。
4.RunLoop即将进入睡眠状态。
5.RunLoop唤醒,但在处理唤醒他的事件之前。
6.退出RunLoop。
我们可以使用Core Foundation将RunLoop观察器添加到应用程序。要创建RunLoop观察器,可以创建CFRunLoopObserverRef不透明类型的实例。此类型会跟踪自定义您的自定义回调函数以及它感兴趣的活动。
与定时器类似,RunLoop观察器可以使用一次或重复使用。一次观察者在执行回调函数后从RunLoop中删除,而重复观察者仍然在RunLoop中。我们可以指定观察者在创建时是运行一次还是重复运行。
有关如何创建运行循环观察程序的示例,请参考2.2配置运行循环。有关参考信息,请参阅CFRunLoopObserver参考
1.2.6:事件的RunLoop执行次序
每次运行RunLoop时,线程的RunLoop都会处理未决事件,并为任何附加的观察者生成通知。它的执行顺序非常具体,如下所示:
1.通知观察者已经进入RunLoop。
2.通知观察者计时器执行准备就绪。
3.通知观察者,非基于端口的输入源即将触发。
4.启动任何可以触发的非基于端口的输入源。
5.如果基于端口的输入源已准备好并正在等待触发,请立即处理该事件。跳转到第9步。
6.通知观察者该线程即将进入休眠。
7.让线程进入休眠状态,直到发生以下事件之一:
<1>一个基于端口的输入源事件到达。
<2>计时器启动。
<3>RunLoop超时。
<4>RunLoop被明确地唤醒。
8.通知观察者线程刚刚唤醒。
9。处理未决事件
<1>如果用户定义的定时器启动,则处理定时器事件并重新启动循环。跳转到第2步。
<2>如果输入源被触发,则交付事件。
<3>如果RunLoop显式唤醒但尚未超时,重新启动RunLoop。跳转到第2步。
10.通知观察者RunLoop已退出。
因为定时器和输入源的观察者通知是在这些事件实际发生前交付的,所以通知时间和实际事件时间之间可能存在差距。如果这些事件之间的时间很关键,则可以使用休眠和从休眠中醒来的通知来完成关联实际事件之间的时间。
由于定时器和其他周期性事件是在运行循环时交付的,因此绕过该RunLoop会中断这些事件的交付。无论何时通过输入一个循环并重复地从应用程序请求事件来实现鼠标跟踪例程,都会发生此行为的典型示例。由于我们的代码直接抓取事件,而不是让应用程序正常调度这些事件,因此在鼠标跟踪例程退出并将控制返回给应用程序之前,激活的定时器将无法触发。
RunLoop可以使用RunLoop对象显式唤醒。其他事件也可能导致RunLoop被唤醒。例如,添加另一个基于非端口的输入源会唤醒RunLoop,以便于可以立即处理输入源,而不是等待其他事件发生。
二:使用RunLoop对象
RunLoop对象提供了将输入源,定时器和运行循环观察器添加到RunLoop并运行它的主要接口。每个线程都有一个与之关联的RunLoop对象。在Cocoa中,这个对象是NSRunLoop类的一个实例。在底层的应用程序中,它是一个指向CFRunLoopRef类型的指针。
2.1.获取RunLoop对象
可以使用如下方式来获取当前线程的RunLoop:
<1>在Cocoa应用程序中,使用NSRunLoop的currentRunLoop类方法来检索NSRunLoop对象。
<2>使用CFRunLoopGetCurrent函数。
虽然它们不是免费的桥接类型(toll-free bridged types),但在需要时,我们可以从NSRunLoop对象获取CFRunLoopRef不透明类型。NSRunLoop类定义了一个getCFRunLoop方法,该方法返回可以传递给Core Foundation例程的CFRunLoopRef类型。由于两个对象都引用同一个RunLoop,因此可以根据需要将调用混合到NSRunLoop对象和CFRunLoopRef不透明类型。
2.2.配置RunLoop
在子线程上运行RunLoop之前,我们必须至少添加一个输入源或计时器。如果RunLoop没有任何监控的来源,当尝试运行RunLoop时,它会立即退出。有关如何将源添加到RunLoop的示例,请参考第三节配置RunLoop源。
除了安装源代码之外,我们还可以安装RunLoop观察器并使用它们来检测RunLoop的不同执行阶段。要安装RunLoop观察器,需要创建一个CFRunLoopObserverRef 不透明类型,并使用CFRunLoopAddObserver函数将其添加到RunLoop中。RunLoop观察者必须使用Core Foundation创建,即使对于Cocoa应用程序也是如此。
图(2-1-1)显示了将RunLoop观察器连接到其RunLoop的线程的主例程。该示例的目的是向您展示如何创建RunLoop观察器,因此代码只需要设置一个RunLoop观察器即可监视所有RunLoop活动。基本处理程序例程只是在处理计时器请求时记录RunLoop活动。
如果我们想配置一个一直运行的RunLoop,最好添加至少一个输入源接收消息。尽管我们可以在进入RunLoop是关联一个定时器,一旦定时器开始,它通常会面临失效,一旦定时器失效RunLoop便会退出。附加重复计时器可以使RunLoop长时间运行,但是会定时触发计时器唤醒线程,这实际上是另一种轮询方式。相反,输入源会等待事件发生,让线程一直处于休眠状态,知道它(事件)发生。
2.3.启动RunLoop
启动RunLoop仅适用于应用程序中的子线程。RunLoop必须至少有一个输入源和计时器才能进行监视。如果没有,则RunLoop立即退出。
有以下几种开启RunLoop的方式,包括:
<1>无条件的
<2>具有设定的时间限制
<3>在特定模式下
无条件的进入RunLoop是最简单的选择,但他也是最不可取的。无条件的运行你的RunLoop会把你的线程放到一个永久循环中,这使你很少控制RunLoop本身。我们可以添加和删除输入源和定时器,但停止RunLoop的唯一方法是杀死它,同样在自定义模式下,我们也无法运行RunLoop。
不使用无条件地运行RunLoop,最好使用超时值运行RunLoop。当我们设置超时值时,RunLoop会一直运行,知道事件到达或分配的时间到期。如果事件到达,则将该事件分派给处理程序进行处理,然后RunLoop退出。我们使用代码重新启动RunLoop来处理下一个事件。如果分配的时间到期,我们可以简单地重新启动RunLoop或使用时间来完成所需的任务管理。
除了超时值之外,我们也可以使用特定模式运行RunLoop。模式和超时值不是互斥的,并且在启动RunLoop时都可以使用。模式限制将事件传递到RunLoop的源的类型,并在RunLoop模式中有更详细的描述。
图(2-1-2)显示了一个线程的主要入口示例的框架版本。这个例子的关键部分显示了RunLoop的基本结构。本质上,我们将输入源和定时器添加到RunLoop中,然后重复调用其中一个示例来启动RunLoop。每次RunLoop示例返回时,都会检查是否有任何可能导致退出线程的情况。该示例使用Core FoundationRunLoop示例,以便它可以检查返回结果并确定RunLoop退出的原因。如果使用Cocoa并且不需要检查返回值,我们也可以使用NSRunLoop类的方法以类似的方式运行RunLoop。(有关调用NSRunLoop类的方法运行循环的示例,请参考图(3-3-3)。)
它可以递归运行一个RunLoop。换句话说,我们可以调用CFRunLoopRun,CFRunLoopRunInMode或任何NSRunLoop方法来从输入源或定时器的处理程序中启动RunLoop。当这样做时,可以使用任何mode运行嵌套的RunLoop,包括外部嵌套使用的模式。
2.4.退出RunLoop
在处理事件之前,有两种方式可以使RunLoop退出:
<1>以超时值配置RunLoop运行。
<2>告诉RunLoop停止。
如果可以管理它,使用超时值肯定是首选。指定超时值可让RunLoop完成所有正常处理,包括在退出之前将通知发送到RunLoop观察器。
使用CFRunLoopStop函数显式停止RunLoop会产生类似于超时的结果。RunLoop发出任何剩余的RunLoop通知,然后退出。不同的是,我们可以在无条件开启的RunLoop中使用此技术。
尽管删除RunLoop的输入源和定时器也可能导致RunLoop退出,但这不是停止RunLoop的可靠方法。一些系统例程将输入源添加到RunLoop以处理所需的事件。但是我们的代码可能没有意识到这些输入源,所以它将无法删除他们,这将阻止RunLoop退出。
2.5.线程安全和RunLoop对象
线程安全取决于我们使用哪个API来操作运行循环。Core Foundation中的函数通常是线程安全的,可以从任何线程中调用。但是,如果要执行更改RunLoop配置的操作,则尽可能从拥有RunLoop的线程执行此操作仍是一种好的做法。
Cocoa 中的NSRunLoop类并不想其核心机处对象那样天生就是线程安全的。如果使用NSRunLoop类来修改RunLoop,则应该只从拥有该RunLoop的同一个线程来完成。将输入源或定时器源添加到属于不同线程的RunLoop中可能会导致代码崩溃或以意外的方式运行。
三:配置RunLoop源
以下部分显示了如何在Cocoa和Core Foundation中设置不同类型的输入源示例。
3.1.定义自定义输入源
创建自定义输入源包括定义以下内容:
<1>希望输入源处理的信息。
<2>调度程序让有兴趣的客户知道如何联系您的输入源。
<3>处理程序例程,用于执行任何客户端发送的请求。
<4>取消例程以使输入源无效。
由于我们创建了一个自定义的输入源来处理自定义的信息,因此实际配置是设计灵活的。调度程序,处理程序和取消例程几乎总是需要用于自定义输入源的关键例程。然而,大部分输入源的行为的其余部分都发生在这些处理程序之外。例如,我们需要定义将数据传递到输入源并将输入源的存在传递给其他线程的机制。
下图(3-1)显示了自定义输入源的示例配置。在本例中,应用程序的主线程保持对输入源,该输入源的自定义命令缓冲区以及安装输入源的RunLoop的引用。当主线程有一个任务想要切换到工作线程时,它将命令发送到命令缓冲区以及工作线程启动任务所需的任何信息。(因为工作线程的主线程和输入源都可以访问命令缓冲区,所以访问必须同步。)一旦命令发布,主线程就会发信号通知输入源并唤醒工作线程的RunLoop。在收到唤醒命令后,RunLoop会调用输入源的处理程序,该输入源处理命令缓冲区中的命令。
下面各节将解释上图中自定义输入源的实现,并显示需要实现的关键代码。
3.1.1.定义输入源
定义自定义输入源需要使用Core Foundation例程来配置RunLoop源并将其附加到RunLoop。虽然基本的处理程序是基于C的函数,但这并不妨碍我们为这些函数编写包装,并使用Objective-C或C++来实现代码的主题。
图3-1中介绍的输入源使用Objective-C对象来管理命令缓冲区并与RunLoop进行协调。图(3-1-1)显示了这个对象的定义。RunLoopSpurce对象管理命令缓冲区并使用该缓冲区接收来自其他线程的消息。此图还显示RunLoopContext对象的自定义,该对象实际上只是一个容器,用于将RunLoopSpurce对象和RunLoop引用传递给应用程序的主线程。
虽然Objective-C代码管理输入源的自定义数据,但将输入源附加到RunLoop中需要使用基于C的回调函数。当我们将RunLoop源实际连接到RunLoop时,将调用其中的第一个函数如图(3-1-2)所示。由于此输入源只有一个客户端(主线程),因此它使用调度程序函数发送消息以在该线程上向应用程序委托注册自己。当委托人想要与输入源通信时,它使用RunLoopContext对象中的信息来执行此操作。
最重要的回调例程之一是用于在输入源发送信号时处理自定义数据的回调例程。图(3-1-3)显示了RunLoopSource对象关联的执行回调示例。该函数只是将请求执行的请求转发给SourceFired方法,然后该方法处理命令缓冲区中存在的任何命令。
如果使用CFRunLoopSourceInvalidate函数将输入源从其RunLoop中移除,系统将调用输入源取得取消例程。我们可以使用此例程来通知客户端输入源不再有效,并且应该删除对它的引用。图(3-1-4)显示了用RunLoopSpurce对象注册的取消回调例程。该函数将另一个RunLoopContext对象发送给应用程序委托,但是这次要求委托移除对RunLoop源的引用。
注意:应用程序委托的registerSource:和removeSource:方法的代码显示在3.1.3节与输入源的客户端协调中。
3.1.2在RunLoop中安装输入源
图3-2-1显示了RunLoopSource类的init和addToCurrentRunLoop方法。init方法创建实际连接到RunLoop的CFRunLoopSourceRef不透明类型。它将RunLoopSource对象本身作为上下文信息传递,以便回调例程具有指向该对象的指针。在工作线程调用addToCurrentRunLoop方法之前,不会发生输入源的安装,此时将调用RunLoopSourceScheduleRoutine回调函数。一旦输入源被添加到RunLoop中,线程就可以运行它的RunLoop来等待它。
3.1.3与输入源的客户端协调
为了使输入源有用,我们需要操作它并从另一个线程发出信号。输入源的全部要点是将其关联的线程休眠直到有事情要做。这个事实需要应用程序中的其他线程知道输入源并且有一个与之通信的方法。
将输入源首次安装在RunLoop中时,向客户端通知输入源的一种方法是发送注册请求。我们可以根据输入源注册尽可能多的客户端,或者可以将其注册到某个中央机构,然后将输入源发布给感兴趣的客户端。图(3-1-6)显示了由应用程序委托定义并在调用RunLoopSource对象的调度程序函数时调用注册方法。该方法接收由RunLoopSource对象提供的RunLoopContext对象,并将其添加到其源列表中。此列表还显示用于在从RunLoop中删除输入源时注销输入源的例程。
3.1.4发送输入源信号
将数据传递到输入源后,客户端必须发出信号并唤醒其RunLoop。信号源让RunLoop知道源已准备好处理。而且因为线程在信号发生时可能会休眠,因此应该始终明确的唤醒RunLoop。如果不这样做,可能会导致处理输入源的延迟。
图(3-2-3)显示了RunLoopSource对象的fireCommandsOnRunLoop方法。当客户端准备好处理添加缓冲区的命令时,客户端会调用此方法。
注意:我们不应该尝试通过发送自定义输入源来处理SIGHUP或其他类型的过程级信号。Core Foundation唤醒RunLoop的方法不是信号安全的,不应该在应用程序的信号处理程序中使用。
3.2.配置定时器源
要创建一个计时器源,首先创建一个计时器对象并将其安排在RunLoop中。在Cocoa中,使用NSTimer类来创建新的计时器对象,在Core Foundation中使用CFRunLoopTimerRef不透明类型。在内部,NSTimer类只是Core Foundation的扩展,它提供了一些便利功能,如使用相同的方法创建和安排定时器的能力。
在Cocoa中,可以使用以下任一类方法创建和安排计时器:
<1>scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
<2>scheduledTimerWithTimeInterval:invocation:repeats:
这些方法创建计时器并将其添加到当前线程的默认模式下的RunLoop(NSDefaultRunLoopMode)。如果想创建一个NSTimer对象,然后使用NSRunLoop的addTimer:forMode:方法将它添加到RunLoop中,也可以手动安排定时器。这两种技术基本上都是相同的,但是对定时器配置级别的控制是不同的。如创建计时器并将其手动添加到RunLoop中则可以使用除默认模式之外的模式执行此操作。图(3-2-1)显示了如何使用这两种技术创建定时器。第一个定时器的初始延迟时间为1秒,但之后每0.1秒定时触发一次。第二个定时器在初始0.2秒延迟后开始执行,然后每0.2秒执行一次。
图(3-2-2)显示了Core Foundation配置计时器所需的代码。虽然此示例没有在上下文中传递任何用户定义的信息,但可以使用此结构来传递计时器所需的自定义数据。
3.3.配置基于端口的输入源
Cocoa和Core Foundation都提供了用于线程间或进程间通信的基于端口的对象。下面介绍如何使用几种不同类型的端口设置端口通信。
3.3.1配置一个NSMachPort对象
要与NSMachPort对象建立本地连接,需要创建端口对象并将其添加到主线程的RunLoop中。启动辅助线程时,将相同的对象传递给线程的入口函数。子线程可以使用相同的对象将消息发送回主线程。
<1>主线程的实现代码
图(3-3-1)显示了启动辅助工作线程的主线程代码。因为Cocoa框架执行许多配置端口和RunLoop的干预步骤,所以lanuchThread方法明显短于其Core Foundation等价物;然而两者的行为几乎完全相同。一个区别是该方法不是将本地端口的名称发送给工作线程,而是直接发送给NSPort对象。
为了在线程之间建立一个双向通信通道,我们可能希望工作线程在check-in消息中发送本地端口到主线程。收到check-in消息后,主线程会知道启动第二线程时一切顺利,并且还提供了一种将更多消息发送到该线程的方法。
图(3-3-2)显示了主线程的handlePortMessage:方法。当数据到达线程自己的本地端口时调用此方法。当check-in消息到达时,该方法直接从端口消息中检索辅助线程的端口并将其保存以供以后使用。
注意:如果您创建的是iOS项目此代码会报错,因为NSPortMessage目前只支持macOS 10.0+之后的系统。
<2>辅助工作线程的实现代码
对于辅助工作线程,必须使用指定的端口配置线程并将信息传回主线程。
图(3-3-3)显示了设置工作线程的代码。为线程创建一个自动释放池,该方法创建一个工作对象来驱动线程执行。工作对象的sendCheckinMessage:方法(图3-3-4)为工作线程创建一个本地端口,并将一个check-in消息发回主线程。
使用NSMachPort时,本地和远程线程可以使用相同的端口对象进行线程之间的单向通信。换句话说,由一个线程创建的本地端口对象成为另一个线程的远程端口对象。
图(3-3-4)显示了辅助线程的check-in示例。此方法为将来的通信设置了自己的本地端口,然后将check-in消息发送回主线程。该方法使用LanuchThreadWithPort:方法中收到的端口对象作为消息的目标。
3.3.2配置一个NSMessagePort对象
要与NSMessagePort对象建立本地连接,我们不能简单地在线程之间传递端口对象。远程消息端口必须按名称获取。在Cocoa中实现这一点需要注册一个特定名称的本地端口,然后将该名称传递给远程线程,以便它可以获取适当的端口对象进行通信。图(3-3-5)显示了使用消息端口的情况下端口的创建和注册过程。
3.3.3在Core Foundation中配置基于端口的输入源
这里介绍如何使用Core Foundation在应用程序的主线程和工作线程之间建立双向通信通道。
图(3-3-6)显示了应用程序主线程调用的启动工作线程的代码。代码首先设置一个CFMessagePortRef不透明类型来侦听来自工作线程的消息。工作线程需要端口的名称来建立连接,以便将字符串值传递给工作线程入口点函数。端口名称在当前用户上下文中通常应该是唯一的;否则,可能会遇到冲突。
在安装了端口并启动了线程的情况下,主线程可以在等待线程check-in时继续执行常规执行。当check-in消息到达时,它将被分派到主线程的MainThreadResponseHandler函数中,如图(3-3-7)。此函数提取工作线程的端口名称并为未来的通信创建管道。
在配置主线程后,剩余的唯一东西是新创建的工作线程创建自己的端口并进行check-in。图(3-3-8)显示了工作线程的入口点函数。该函数提取主线程的端口名称并使用它来创建远程连接回主线程。然后该函数为自己创建一个本地端口,在该线程的RunLoop中安装端口,并向包含本地端口名称的主线程发送一个check-in消息。
一旦它进入RunLoop,发送到线程端口的所有未来事件都将有ProcessClientRequest函数处理。该函数的实现取决于线程所执行的工作类型,在此不显示。