码迷,mamicode.com
首页 > 其他好文 > 详细

运行循环(Run Loops)

时间:2015-02-26 10:06:48      阅读:341      评论:0      收藏:0      [点我收藏+]

标签:run loops   thread   timer   input source   thread safety   

运行循环是与线程相关的基础工具的一部分。运行循环是一个用来安排工作并协调传入事件接收的一个事件处理循环。运行循环的目的是当需要工作的时候,让你的线程处于忙碌状态;当没有工作时,让你的线程处于睡眠状态。

运行循环管理并不是完全自动运行,你必须设计线程代码在适当的时间启动运行循环并响应传入的事件。Cocoa和核心基础提供运行循环对象来帮助你配置和管理你的线程运行循环。你的引用不需要显式的创建这些对象,每个线程,包括应用的主线程都有一个关联的运行循环对象。然而,只有次要线程需要显式的运行他们的运行循环。应用框架自动设置并在主线程运行运行循环,作为启动引用过程的一部分。

以下章节提供关于运行循环以及如何为应用配置运行循环的信息。关于运行循环对象的附加信息,参阅NSRunLoop类引用( NSRunLoop Class Reference)和CFRunLoop 引用(CFRunLoop Reference)。

剖析运行循环

运行循环就如名称一样。运行循环是线程进入和用来运行事件处理程序来响应传入的事件的循环。你的代码提供控制语句用来实现实际运行循环的一部分,换句话说,你的代码提供whilefor循环来驱动循环。在你的循环中,你使用一个运行循环对象来运行事件处理代码用于接收事件并调用处理程序。

运行后接收事件有两种不同类型的来源。输入源交付异步事件,通常从另一个线程或另一个不同的应用发送消息。计时器来源交付同步事件,发生在一个预定时间或重复间隔。当事件到达,两种类型的来源使用特定应用的处理程序来处理事件。

图3-1 展示了一个运行循环的概念结构和各种来源。输入源交付异步事件到相应处理程序,引起runUntilDate: 方法(称为线程相关的NSRunLoop 对象)并退出。计时器来源交付事件到处理程序单不会引起运行循环退出。

3-1 运行循环结构及其来源

 技术分享

除了处理输入来源,运行循环也生成关于运行循环行为的通知。注册为运行循环的观察者可以接受这些通知并使用它们在线程上进行额外的处理。你使用核心基础在你线程上安装运行循环。

以下章节提供关于运行循环组件和它们操作模式的信息。它们还描述了在处理事件过程中不同时间生成的通知。

运行循环模式

运行循环是一个输入源和监视定时器的集合,也是通知运行循环观察者的集合。每次运行你的运行循环,你制定(显式或隐式)运行一个特定的模式。在传递运行循环过程中,只有与该模式管理的来源才能被监控并允许交付。(同样,只有与该模式关联的观察者才能被通知运行循环进程。)与其他模式关联的来源在任何事件发生时都等一下直到传入的合适模式的循环。

在你的代码中,你通过名字来识别模式。Cocoa和核心基础定义一个默认的模式和几种常用的模式,在你的代码中通过字符串指定模式。你可以定义自定义模式,只需要一个自定义字符串指定模式名称。尽管你指定到自定义模式的名字是任意的,这些模式的内容不是任意的。你必须添加一个或多个输入源,计时器或运行循环观察者到任何你创建的模式,以确保它们有用。

在运行循环特定途径中,你使用模式来过滤掉不必要来源。大多数时候,你会希望以系统定义的默认模式来运行你的运行循环。然而,模态面板可能运行在模态模式。在这种模式中,只有与模态面板相关的资源会交付事件到线程。对于次要线程,你可以使用自定义模式在时间关键型操作期间来防止低优先级来源交付事件。

注意:模式区分基于事件的来源而非事件的类型。例如,你不会使用模式来匹配鼠标单击事件或键盘事件。你可以使用模式来监听一组不同的端口,暂时暂停计时器,或以其他方式改变来源和当前监控的运行循环观察者。

表3-1列出了Cocoa和核心基础定义的标准模式以及你使用模式的描述。这个名词列表列出了在你的代码中用来指定模式的实际常量。

表3-1 预定义的运行循环模式

模式

名称

说明

默认

NSDefaultRunLoopMode(Cocoa)

kCFRunLoopDefaultMode (Core Foundation)

默认模式用于大多数操作。大多数情况下,你使用该模式来启动你的运行循环并配置你的输入源。

连接

NSConnectionReplyMode(Cocoa)

Cocoa结合NSConnection 对象使用该模式来监控回答。你自己应该很少需要使用这个模式。

模态

NSModalPanelRunLoopMode(Cocoa)

Cocoa使用该模式来识别用于模态面板的事件。

事件跟踪

NSEventTrackingRunLoopMode(Cocoa)

Cocoa使用该模式在鼠标拖动期间来限制传入的事件和其他类型用户界面跟踪循环。

常见模式

NSRunLoopCommonModes(Cocoa)

kCFRunLoopCommonModes (Core Foundation)

只是一个常用模式的可配置组。将输入源与这种模式结合也将它与组中其他模式结合。对于Cocoa引用,这组默认包括默认、模态和时间跟踪模式。核心基础包括的只是默认模式。你可以使用CFRunLoopAddCommonMode 函数添加自定义模式。

 

输入源

输入源异步的交付事件到你的线程。事件来源取决于输入源的类型,通常是两类之一。基于端口输入源监控你的应用的Mach端口。自定义输入源监控自定义来源的事件。对于运行循环而言,他不管输入源是基于端口的还是自定义的。系统通常实现两种类型的输入源,你都可以使用。两种来与唯一区别是它们如何被信号通知。基于端口来源由内核自动发信号,自定义来源必须手动的从另一个线程发信号。

当你创建一个输入源,你将它分配为运行循环中的一个或多个模式。模式影响任何给定时间监控的输入源。大多数情况下,你以默认模式运行你的运行循环,但你可以指定自定义模式。如果一个输入源不在当前监控的模式下,它生成的任何事件都被放置知道运行循环在正确的模式下运行。

以下章节描述部分输入源。

基于端口来源

Cocoa和核心框架为使用端口相关的对象和函数来创建基于端口来源提供内置支持。例如,在Cocoa,你从未直接创建一个输入源。你只需创建一个端口对象并使用NSPort 方法来添加该端口到运行循环。端口对象处理你需要的输入源的创建和配置。

在核心基础中,你必须手动创建端口和运行循环来源。在这两种情况,你可以使用不透明类型端口(CFMachPortRef,CFMessagePortRef CFSocketRef)相关函数来创建合适的对象。

如何设置和配置自定义基于端口来源的例子,参见配置一个基于端口输入源(Configuring a Port-Based Input Source)。

自定义输入来源

为了创建一个自定义输入来源,你必须使用核心基础中与CFRunLoopSourceRef 不透明类型相关的函数。你可以使用几个毁掉函数来配置一个自定义输入源。核心基础调用在不同点上调用这些函数来配置来源,处理任何传入的事件,在被运行循环移除时拆除来源。

除了当事件发生时定义自定义来源行为,你还必须定义事件交付机制。这部分来源运行在一个单独的线程并负责提供输入源数据,在数据准备处理时用信号通知。事件交付机制由你决定但不会过于复杂。

如何创建一个自定义输入源的例子,参见定义一个自定义输入源(Defining a Custom Input Source)。自定义输入源参考信息,参见CFRunLoopSource 参考(CFRunLoopSource Reference)。

Cocoa执行选择器来源

除了基于端口来源,Cocoa定义自定义输入源,允许你在任何线程上执行一个选择器。类似基于端口来源,执行选择器需要在目标线程上进行序列化,减轻在一个线程上运行的多个方法可能发生的同步问题。与基于端口来源不同的是,执行选择器来源在执行完选择器后会从运行循环上移除自己。

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

当在另一个线程上执行一个选择器,目标线程必须有一个活跃的运行循环。对于你创建的线程,这意味着等待直到你代码显式的启动运行循环。因为主线程开始它自己的运行循环,然而,只要应用调用应用代理的applicationDidFinishLaunching: 方法,你可以开始在该线程上调用。运行循环处理所有排队执行选择器,在每次循环调用,而不是在每个循环迭代处理一个。

表3-2 列出NSObject 定义的方法,可用于执行其他线程上的选择器。因为NSObject 声明这些方法,你可以从任何可访问对象的 Objective-C线程,包括POSIX线程来使用它们。这些方法并不创建一个新线程来执行选择器。

表3-2 在其他线程上执行选择器

方法

说明

performSelectorOnMainThread:withObject:waitUntilDone:

performSelectorOnMainThread:withObject:waitUntilDone:modes:

在线程的下个运行循环周期中,执行在应用主线程上的指定选择器。这个方法给你阻塞当前线程的选择直到选择器被执行。

performSelector:onThread:withObject:waitUntilDone:

performSelector:onThread:withObject:waitUntilDone:modes:

在有NSThread 对象的线程上执行指定选择器。执行方法给你阻塞当前线程的选择直到选择器被执行。

performSelector:withObject:afterDelay:

performSelector:withObject:afterDelay:inModes:

在下个运行循环周期和可选延迟周期中,执行当前线程上的指定选择器。因为它将等待下个运行循环周期来执行选择器,这些方法提供一个从当前执行代码自动迷你延迟。多队列选择器一个接一个排队执行。

cancelPreviousPerformRequestsWithTarget:

cancelPreviousPerformRequestsWithTarget:selector:object:

使用performSelector:withObject:afterDelay: 或performSelector:withObject:afterDelay:inModes:方法取消发送消息到当前线程。

关于这些方法的详细信息,参见NSObject 类参考(NSObject Class Reference)。

计时器来源

在未来某个预设的时间,计时器来源同步交付事件到你的线程。计时器是线程通知自己做点事情的一种方式。例如,搜索字段可以使用计时器在用户输入关键字一定时间过去后启动一个自动搜索。使用这种延迟时间让用户在开始搜索前有更多的机会来输入搜索字符串。

虽然它产生自基于时间的通知,一个计时器不是一个实时机制。像输入源,计时器与你运行循环的特定模式有关。如果一个计时器不是当前运行循环监控的模式,它不触发直到你运行循环在计时器支持的一个模式上。同样的,如果在运行循环在执行处理程序过程中计时器触发,计时器会等待直到下次运行循环调用处理程序。如果运行循环不允许,计时器不会触发。

你可以配置计时器来一次或多次重复产生事件。一个重复计时器基于预定触发时间自动重新安排,而非实际触发时间。例如,如果计时器在特定时间后每隔5秒触发,预定触发时间会落在原来5秒间隔上,即使实际触发时间呗延迟。如果触发时间被延迟太多以至错过一个或多个预定触发时间,计时器在错过的那段时间内只触发一次。在错过时间内触发后,计时器按照下个预定触发时间重新安排。

关于配置计时器来源的更多信息,参见配置计时器来源(Configuring Timer Sources)。关于参考信息,参见NSTimer 类参考(NSTimer Class Reference)或CFRunLoopTimer 参考(CFRunLoopTimer Reference

运行循环观察者

与来源相比,在适当的同步或异步事件发生时触发,在执行运行循环本身执行中,运行循环观察者在特定地点触发。你可以使用运行循环观察者让你的线程准备处理一个给定事件或在线程进入休眠状态前准备。你可以将运行循环观察者和运行循环中的以下事件结合起来:

  • 运行循环入口。
  • 当运行循环开始处理计时器。
  • 当运行循环开始处理输入源。
  • 当运行循环开始进入休眠。
  • 当运行循环被唤醒,在处理事件前将其唤醒。
  • 退出运行循环。

你可以使用核心基础往应用中添加运行循环观察者。为了创建一个运行循环观察者,你可以创建一个CFRunLoopObserverRef 不透明类型的新实例。该类型跟踪你自定义回调函数和感兴趣的活动。

类似于计时器,运行循环观察者可以一次或多次使用。一个一次性观察者在它触发后从运行循环上移除自己,然而一个重复观察者仍然连接。当你创建观察者时可以指定该观察者是运行一次还是多次。

如何创建一个运行循环观察者的例子,参见配置运行循环(Configuring the Run Loop)。关于参考信息,参见CFRunLoopObserver 参考(CFRunLoopObserver Reference)。

事件的运行循环序列

每次你运行,线程的运行循环处理等待事件并生成通知附加观察者。顺序如下:

  • 通知观察者已进入运行循环。
  • 通知观察者任何准备的计时器将要触发。
  • 通知观察者任何不基于端口的输入源将要触发。
  • 触发任何不基于端口准备触发的输入源。
  • 如果基于端口输入源准备就绪等待触发,立即处理事件。跳转到第9步。
  • 通知观察者线程将要休眠。
  • 让线程休眠直到以下事件发生:

    • 一个事件到达基于端口的输入源。
    • 计时器触发。
    • 为运行循环到期设置的超时值。
    • 运行循环显式的唤醒
  • 通知观察者线程唤醒
  • 处理等待事件
    • 如果一个用户定义的计时器触发,处理计时器事件并重新启动循环。跳转到步骤2.
    • 如果一个输入源触发,交付事件。
    • 如果运行循环显式的唤醒但尚未超时,重新启动循环,跳转到步骤2.
  • 通知观察者运行循环已退出。

因为计时器和输入源的观察者通知在事件发生前被交付,可能通知时间与实际发生的时间有差距。如果这些事件间的时间至关重要,你可以使用休眠和从休眠到唤醒的通知来帮助你关联实际事件间的时间。

因为计时器和其他定期事件在你运行运行循环时被交付,注意循环会破坏这些事件的交付。当你通过输入一个循环并向应用多次请求事件来实现一个鼠标跟踪程序时,会发生这种行为。因为你的代码直接抓住事件,而非让应用正常调度这些事件,活动的计时器可能不能被触发知道你的鼠标跟踪程序退出并返回让应用控制。

一个运行循环可以用运行循环对象显式的唤醒。其他活动也可能导致运行循环被唤醒。例如,添加另一个非基于端口的输入源来唤醒运行循环,这样可以立即处理输入源,而不是等到其他事件发生。

什么时候可以使用一个运行循环?

当你为你的应用创建次要线程时,是你唯一一次需要显式的运行一个运行循环。应用主线程的运行循环是一个重要的基础设施。因此,应用框架提供代码来运行主要应用循环并自动启动循环。iOSUIApplication 的run 方法(或者OS X中的NSApplication )启动一个应用的主循环作为正常启动序列的一部分。如果你使用Xcode项目模板来创建你的应用,你不应该显式的调用这些程序。

对于次要线程,你需要决定运行循环是否必要的,如果是,你自己配置并启用它。你不需要在所有情况下启动一个线程的运行循环。例如,如果你使用一个线程来执行一些长期和预定的任务,你就能避免启动运行循环。运行循环用于你想与线程有更多交互的情况。例如,你需要启动一个运行循环如果你计划做以下事情:

  • 使用端口或自定义输入源来与其他线程通信。
  • 在线程上使用计时器。
  • 在Cocoa用于中使用任何performSelector方法。
  • 使线程执行周期任务

如果你选择使用运行循环,配置和设置是很简单的。与所有线程编程一样,你应该有个计划在适当的情况下退出次要线程。让它退出比强迫它终止可以更好的更干净的结束线程。如何配置信息和退出运行循环的信息参见使用运行循环对象( Using Run Loop Objects)。

使用运行循环对象

运行循环对象为主要界面提供添加输入源、计时器和运行循环观察者到你的运行循环并运行之。每个线程都有一个单独的运行循环对象与之相关联。在Cocoa,这个对象是NSRunLoop 类的一个实例。在底层应用中,它是一个指向CFRunLoopRef 不透明类型的指针。

获取运行循环对象

为了获取当前线程的运行循环,你可以使用下列方法之一:

虽然他们不是免费桥接类型,当需要时,你可以从NSRunLoop 对象获取一个CFRunLoopRef 不透明类型。NSRunLoop 类定义了一个getCFRunLoop 方法,返回一个CFRunLoopRef 类型,你可以传递到核心基础程序。因为两个对象引用相同运行循环,如果需要你可以混合调用NSRunLoop 对象和CFRunLoopRef 不透明类型。

配置运行循环

在次要线程运行一个运行循环之前,你必须添加至少一个输入源或计时器到运行循环上。如果一个运行循环没有任何来源要监控,当你试图运行它时,它会立即退出。如何往运行循环上添加来源的例子,见配置运行循环来源(Configuring Run Loop Sources)。

除了安装来源,你可以安装运行循环观察者并使用它们来监测运行循环的不同执行阶段。为了安装一个运行循环观察者,创建一个CFRunLoopObserverRef 不透明类型并使用CFRunLoopAddObserver 函数来添加到你的运行循环上。运行循环观察者必须使用核心基础来创建,即便是Cocoa应用。

清单3-1展示了线程的主要程序添加到运行循环的运行循环观察者上。例子的目的是向你展示如何创建运行循环观察者,因此代码简单的设置了一个运行循环来监视所有运行循环活动。基本处理程序(未展示)简单的记录运行循环活动,因为它处理计时器请求。

清单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);
}
当为一个长期线程配置运行循环,最好添加至少一个输入源来接收消息。尽管你可以仅添加一个计时器来进入运行新,一旦计时器触发后,它通常是无效的,这将导致运行循环退出。附加一个重复的计时器可以保持运行循环运行一段较长的时间,但会涉及到周期性的触发计时器唤醒你的线程,这实际上是另一种形式的轮询。相比之下,一个输入源等待事件发生,保持你的线程休眠知道它完成。

启动运行循环

只有你应用中的次要线程才需要启动运行循环。一个运行循环必须至少有一个输入源或计时器用于监控。如果没有,运行循环将立即退出。

有以下几种方法来启动运行循环:

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

无条件的进入你的运行循环是最简单的选择,但也是最不可取的。无条件的运行你的运行循环将线程放置到一个永久循环,你对运行循环本身只有很少的控制。你可以添加和删除输入源和计时器,但停止运行循环的唯一方法是杀死它。也没有一个办法来自定义模式下运行运行循环。

最好是运行一个有超时值的运行循环,而不是无条件的允许。当你使用一个超时值时,运行循环运行知道一个事件到达或者分配的时间过期。如果一个事件到达,该事件被分配到一个处理程序来处理,运行循环退出。你的代码可以重新启动运行循环来处理下一个事件。如果分配的时间过期,你可以简单的重启运行循环或使用该时间来做任何家务。

除了一个超时值,你也可以使用特定模式来运行你的运行循环。模式和超时值并不互相排斥,在启动运行循环时都可以使用。模式限制交付到运行循环事件的来源类型,详情见运行循环模式(Run Loop Modes)。

清单3-2 展示了线程主要入口程序的骨架版本。这个例子的关键部分展示了运行循环的基本结构。从本质上说,你添加输入源和计时器到运行循环,然后反复调用程序来启动运行循环。每次运行循环程序返回,检查是否出现任何条件批准线程退出。例子使用核心基础运行循环程序,这样可以检查返回结果并确定运行循环退出的原因。你也可以使用NSRunLoop 类的方法以相同的方式来运行运行循环,如果你使用Cocoa则不需要检查返回值。(调用NSRunLoop 类方法的运行循环例子,见清单3-14.

清单3-2 运行一个运行循环

- (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.
}

可以递归的运行一个运行循环。换句话说,你可以调用CFRunLoopRunCFRunLoopRunInMode或任何NSRunLoop 方法来启动运行循环,在其中有输入源或计时器的处理程序。当这样做时,你可以使用任何你想要的方式来运行嵌套的运行循环,包括在运行循环外使用的模式。

退出运行循环

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

  • 配置运行循环一个超时值。
  • 告知运行循环停止。

使用一个超时值当然是首选,如果你可以管理它。指定一个超时值,让运行循环完成所有正常进程,包括在退出前交付通知到运行循环观察者。

显式的使用CFRunLoopStop 函数停止运行循环产生的结果类似于超时。运行循环发送任何剩余运行循环通知然后退出。不同的是你可以在无条件启动运行循环时使用此技术。

尽管删除运行循环的输入源和计时器也可能导致运行循环退出,这并不是一个可靠的方法来停止运行新。一些系统程序添加输入源到运行循环来处理事件。因为你的代码不可能意识到这些输入源,它们将无法删除这些,这将阻止运行循环退出。

线程安全和运行循环对象

线程安全变化取决于你使用哪个API来操作你的运行循环。核心基础中的函数通常都是线程安全的,可以从任何线程调用。如果你执行操作来改变运行循环的配置,然而,对于拥有运行循环的线程,这样做仍然是最佳实践。

Cocoa NSRunLoop 类不像核心基础副本本身有线程安全。如果你使用NSRunLoop 类来修改运行循环,只有自己拥有运行循环的线程你才能这么做。添加一个输入源或计时器到属于不同线程的运行循环上可能导致代码崩溃或一种意想不到的行为。

配置运行循环来源

以下章节展示了在Cocoa和核心基础中如何设置不同类型的输入源的例子

定义一个自定义输入源

创建一个自定义输入源包括定义如下内容:

  • 你希望你输入源处理的信息。
  • 调度程序让感兴趣的客户端知道如何接触到你的输入源。
  • 一个处理程序来执行任何客户端发送的请求。
  • 取消程序使你的输入源无效。

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

图3-2 展示了配置自定义输入源的例子。在这个例子中,应用的主线程维护输入源的引用,输入源的自定义命令缓冲区和输入源安装的运行循环。当主线程上有个任务,希望可以切换到工作线程,它布置一个命令到命令缓冲区以及工作线程启动该任务所需的其他信息。(因为主线程和工作线程的输入源可以访问命令缓冲区,访问必须同步的。)一旦发布命令,主线程发信号到输入源并唤醒工作线程的运行循环。接收到唤醒命令,运行循环调用输入源的处理程序,该处理程序处理命令缓冲区中的命令。

3-2 操作一个自定义输入源

 技术分享

以下章节从前面的图解释了自定义输入源的实现,并展示了你需要实现的关键代码。

定义输入源

定义一个自定义输入源需要使用核心基础程序来配置你的运行循环来源并附加到运行循环上。尽管基本处理程序是基于C的函数,但这并不妨碍你为这些函数写包装函数并使用Objective-C或C++来实现代码的主体。

图3-2中介绍的输入源使用一个Objective-C对象来管理命令缓冲区并与运行循环协调。清单3-3 展示了该对象的定义。RunLoopSource 对象管理命令缓冲区,并使用缓冲区来接收其他线程的消息。这个清单也展示了RunLoopContext 对象的定义,该对象只是一个容器对象用于传递RunLoopSource 对象和应用主线程引用的运行循环。

清单3-3 自定义输入源对象定义

@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 对象的信息来完成。

清单3-4 调度运行循环来源

@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


当你输入源被发送信号时,用于处理自定义数据的回调程序是最重要的一个。清单3-5展示了执行与RunLoopSource 对象相关的回调程序。该函数简单的将请求转发给sourceFired 方法,然后处理命令缓冲区中的命令。

清单3-5 在输入源执行工作

void RunLoopSourcePerformRoutine (void *info)
{
    RunLoopSource*  obj = (RunLoopSource*)info;
    [obj sourceFired];
}


如果使用CFRunLoopSourceInvalidate 函数将输入源从运行循环删除,系统调用你的输入源取消程序。你可以使用这个程序来通知客户端你的输入源不再有效,并且它们应该删除有关的引用。清单3-6展示了注册为RunLoopSource 对象的取消回调程序。该函数发送另一个RunLoopContext 对象到应用代理,但这次访问代理是为了删除运行循环来源的引用。

清单3-6 作废输入源

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];
}


注意:应用代理的registerSource: 和removeSource: 方法的代码见与客户端协调输入源(Coordinating with Clients of the Input Source)。

在运行循环上安装输入源

清单3-7 展示了RunLoopSource 类的init 和addToCurrentRunLoop 方法。init 方法创建CFRunLoopSourceRef 不透明对象,必须附加到运行循环。它传递RunLoopSource 对象本身作为环境信息,以便回调程序有一个指向该对象的指针。输入源的安装不会发生直到工作线程调用addToCurrentRunLoop 方法,该方法指向RunLoopSourceScheduleRoutine 回调函数。一旦输入源添加到运行循环上,线程可以运行它的运行循环。

清单3-7 安装运行循环来源

- (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 对象调度函数被调用时调用。该方法接收RunLoopSource 对象提供的RunLoopContext 对象,并将其添加到它的来源列表。该清单还展示了当输入源从运行循环上删除时,用于注销输入源的程序。

清单3-8 注册和删除应用代理的输入源

- (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展示了RunLoopSource 对象的fireCommandsOnRunLoop 方法。当客户端准备处理添加到缓冲区的命令,客户端调用这个方法。

清单3-9 唤醒运行循环

- (void)fireCommandsOnRunLoop:(CFRunLoopRef)runloop
{
    CFRunLoopSourceSignal(runLoopSource);
    CFRunLoopWakeUp(runloop);
}

注意:你不应该通过消息传递一个自定义输入源,尝试处理一个SIGHUP 或其他类型的流程级信号。为唤醒运行循环的核心基础框架函数不是信号安全的,不应该用于你的应用信号处理程序内部。关于信号处理程序的更多信息,参见sigaction 操作说明。

配置计时器来源

为了创建一个计时器来源,所有你需要做的是创建一个计时器对象并安排其到你的运行循环上。在Cocoa中,你使用NSTimer 类来创建新计时器对象,在核心基础中你使用CFRunLoopTimerRef 不透明对象。在内部,NSTimer 类是核心框架的一个扩展,提供了一些便利的特性,可以使用相同的方法来创建和安排计时器。

在Cocoa中,你可以使用以下其中一种方法来一次性创建和安排计时器:

这些方法创建计时器并以默认模式(NSDefaultRunLoopMode)将其添加到当前线程的运行循环上。你也可以手动的安排一个计时器如果你希望通过创建你的NSTimer 对象然后使用NSRunLoop的 addTimer:forMode: 方法将其添加到运行循环上。两种技术基本上做同样的事情,但给你不同层次的控制计时器的配置。例如,如果你创建计时器并手动的将其添加到运行循环上,你可以使用其他模式而不是默认模式。清单3-10 展示了如何使用这两种技术来创建计时器。第一个计时器初始延迟1秒,但有规律的每0.1秒触发。第二个计时器在最初0.2秒的延迟后开始触发,然后每0.2秒触发一次。

清单3-10 使用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展示了使用核心基础函数配置一个计时器需要使用的代码。尽管这个例子不传递环境结构中任何用户定义的信息,你可以使用这个结构传递任何计时器需要的自定义数据。关于这种结构的更多信息,参见CFRunLoopTimer 参考(CFRunLoopTimer Reference)。

清单3-11 使用核心基础创建和安排一个计时器

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和核心基础提供基于端口对象来完成线程或进程间的通信。以下章节展示了如何使用集中不同类型的端口来设置端口通信。

配置一个NSMachPort 对象

为了建立与NSMachPort 对象的本地连接,创建端口对象并将其添加到你的主要线程的运行循环上。当启动次要线程时,传递相同的对象到你线程的入口点函数。次要线程可以使用相同的对象来发送消息到你的主要线程。

实现主线程代码

清单3-12 展示了启动次要工作线程的主要线程代码。因为Cocoa框架执行了配置端口和运行循环的许多干预步骤,launchThread 方法明显比核心基础中等效方法(清单3-17)短,然而,这两者的行为几乎是相同的。一个区别是,该方法直接发送NSPort 对象,而非发送工作线程的本地端口的名称。

清单3-12 主线程的启动方法

- (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];
    }
}

为了建立线程间的双向通信信道,你可能希望工作线程以登记消息发送自己本地端口到你的主线程。接收到登记消息使你的主线程知道启动次要线程一切顺利,也为你提供一种方法来发送进一步消息到另一个线程。

清单3-13 展示了主线程的handlePortMessage:方法。当数据到达线程的本地端口时调用该方法。当登记消息到达时,该方法直接从端口消息检索端口用于次要线程,并保存以供以后使用。

清单3-13 处理Mach端口消息

#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.
    }
}


实现次要线程代码

对于次要工作线程,你必须配置线程并使用指定端口将通信信息返回到主线程。

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

清单3-14 使用Mach端口启动工作线程

+(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 展示了次要线程的登记程序。该方法设置自己本地端口用于未来通信,并发送一个登记信息返回到主线程。该方法使用LaunchThreadWithPort: 方法接收到的端口对象作为消息的目标。

清单3-15 使用Mach端口发送登记消息

// 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 展示了你希望使用消息端口情况下端口的创建和注册过程。

清单3-16 注册消息端口

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];


在核心基础中配置一个基于端口的输入源

以下章节展示了如何使用核心基础来设置应用主线程与工作线程间的双向通信信道。

清单3-17 展示了应用主线程启动工作线程的代码调用。代码做的第一件事情是建立一个CFMessagePortRef 不透明类型从工作线程监听消息。工作线程需要端口的名称来进行连接,以便字符串交付到工作线程的入口函数。端口名称通常在当前用户环境是唯一的,否则,你可能遇到冲突。

清单3-17 附加核心基础消息端口到一个新线程

#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 函数,见清单3-18。该函数提取端口名称用于工作线程并创建一个管道用于未来通信。

清单3-18 接收登记信息

#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;
}

配置好主线程,剩下唯一的事情就是为新创建的工作线程创建自己的端口和登记。清单3-19展示了工作线程的入口函数。该函数提取主线程的端口名称并使用它来创建一个远程连接返回到主线程。该函数创建一个本地端口,安装端口到线程的运行循环,并将登记消息包括本地端口名称发送到主线程。

清单3-19 设置线程结构

#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;
}

一旦它进入运行循环,所有未来事件发送到线程端口由ProcessClientRequest 函数处理。该函数的实现取决于线程完成工作的种类,在这里并没有展示。

 

官方原文地址:

https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW1 


运行循环(Run Loops)

运行循环是与线程相关的基础工具的一部分。运行循环是一个用来安排工作并协调传入事件接收的一个事件处理循环。运行循环的目的是当需要工作的时候,让你的线程处于忙碌状态;当没有工作时,让你的线程处于睡眠状态。

运行循环管理并不是完全自动运行,你必须设计线程代码在适当的时间启动运行循环并响应传入的事件。Cocoa和核心基础提供运行循环对象来帮助你配置和管理你的线程运行循环。你的引用不需要显式的创建这些对象,每个线程,包括应用的主线程都有一个关联的运行循环对象。然而,只有次要线程需要显式的运行他们的运行循环。应用框架自动设置并在主线程运行运行循环,作为启动引用过程的一部分。

以下章节提供关于运行循环以及如何为应用配置运行循环的信息。关于运行循环对象的附加信息,参阅NSRunLoop类引用( NSRunLoop Class Reference)和CFRunLoop 引用(CFRunLoop Reference)。

剖析运行循环

运行循环就如名称一样。运行循环是线程进入和用来运行事件处理程序来响应传入的事件的循环。你的代码提供控制语句用来实现实际运行循环的一部分,换句话说,你的代码提供whilefor循环来驱动循环。在你的循环中,你使用一个运行循环对象来运行事件处理代码用于接收事件并调用处理程序。

运行后接收事件有两种不同类型的来源。输入源交付异步事件,通常从另一个线程或另一个不同的应用发送消息。计时器来源交付同步事件,发生在一个预定时间或重复间隔。当事件到达,两种类型的来源使用特定应用的处理程序来处理事件。

图3-1 展示了一个运行循环的概念结构和各种来源。输入源交付异步事件到相应处理程序,引起runUntilDate: 方法(称为线程相关的NSRunLoop 对象)并退出。计时器来源交付事件到处理程序单不会引起运行循环退出。

3-1 运行循环结构及其来源

 技术分享

除了处理输入来源,运行循环也生成关于运行循环行为的通知。注册为运行循环的观察者可以接受这些通知并使用它们在线程上进行额外的处理。你使用核心基础在你线程上安装运行循环。

以下章节提供关于运行循环组件和它们操作模式的信息。它们还描述了在处理事件过程中不同时间生成的通知。

运行循环模式

运行循环是一个输入源和监视定时器的集合,也是通知运行循环观察者的集合。每次运行你的运行循环,你制定(显式或隐式)运行一个特定的模式。在传递运行循环过程中,只有与该模式管理的来源才能被监控并允许交付。(同样,只有与该模式关联的观察者才能被通知运行循环进程。)与其他模式关联的来源在任何事件发生时都等一下直到传入的合适模式的循环。

在你的代码中,你通过名字来识别模式。Cocoa和核心基础定义一个默认的模式和几种常用的模式,在你的代码中通过字符串指定模式。你可以定义自定义模式,只需要一个自定义字符串指定模式名称。尽管你指定到自定义模式的名字是任意的,这些模式的内容不是任意的。你必须添加一个或多个输入源,计时器或运行循环观察者到任何你创建的模式,以确保它们有用。

在运行循环特定途径中,你使用模式来过滤掉不必要来源。大多数时候,你会希望以系统定义的默认模式来运行你的运行循环。然而,模态面板可能运行在模态模式。在这种模式中,只有与模态面板相关的资源会交付事件到线程。对于次要线程,你可以使用自定义模式在时间关键型操作期间来防止低优先级来源交付事件。

注意:模式区分基于事件的来源而非事件的类型。例如,你不会使用模式来匹配鼠标单击事件或键盘事件。你可以使用模式来监听一组不同的端口,暂时暂停计时器,或以其他方式改变来源和当前监控的运行循环观察者。

表3-1列出了Cocoa和核心基础定义的标准模式以及你使用模式的描述。这个名词列表列出了在你的代码中用来指定模式的实际常量。

表3-1 预定义的运行循环模式

模式

名称

说明

默认

NSDefaultRunLoopMode(Cocoa)

kCFRunLoopDefaultMode (Core Foundation)

默认模式用于大多数操作。大多数情况下,你使用该模式来启动你的运行循环并配置你的输入源。

连接

NSConnectionReplyMode(Cocoa)

Cocoa结合NSConnection 对象使用该模式来监控回答。你自己应该很少需要使用这个模式。

模态

NSModalPanelRunLoopMode(Cocoa)

Cocoa使用该模式来识别用于模态面板的事件。

事件跟踪

NSEventTrackingRunLoopMode(Cocoa)

Cocoa使用该模式在鼠标拖动期间来限制传入的事件和其他类型用户界面跟踪循环。

常见模式

NSRunLoopCommonModes(Cocoa)

kCFRunLoopCommonModes (Core Foundation)

只是一个常用模式的可配置组。将输入源与这种模式结合也将它与组中其他模式结合。对于Cocoa引用,这组默认包括默认、模态和时间跟踪模式。核心基础包括的只是默认模式。你可以使用CFRunLoopAddCommonMode 函数添加自定义模式。

 

输入源

输入源异步的交付事件到你的线程。事件来源取决于输入源的类型,通常是两类之一。基于端口输入源监控你的应用的Mach端口。自定义输入源监控自定义来源的事件。对于运行循环而言,他不管输入源是基于端口的还是自定义的。系统通常实现两种类型的输入源,你都可以使用。两种来与唯一区别是它们如何被信号通知。基于端口来源由内核自动发信号,自定义来源必须手动的从另一个线程发信号。

当你创建一个输入源,你将它分配为运行循环中的一个或多个模式。模式影响任何给定时间监控的输入源。大多数情况下,你以默认模式运行你的运行循环,但你可以指定自定义模式。如果一个输入源不在当前监控的模式下,它生成的任何事件都被放置知道运行循环在正确的模式下运行。

以下章节描述部分输入源。

基于端口来源

Cocoa和核心框架为使用端口相关的对象和函数来创建基于端口来源提供内置支持。例如,在Cocoa,你从未直接创建一个输入源。你只需创建一个端口对象并使用NSPort 方法来添加该端口到运行循环。端口对象处理你需要的输入源的创建和配置。

在核心基础中,你必须手动创建端口和运行循环来源。在这两种情况,你可以使用不透明类型端口(CFMachPortRef,CFMessagePortRef CFSocketRef)相关函数来创建合适的对象。

如何设置和配置自定义基于端口来源的例子,参见配置一个基于端口输入源(Configuring a Port-Based Input Source)。

自定义输入来源

为了创建一个自定义输入来源,你必须使用核心基础中与CFRunLoopSourceRef 不透明类型相关的函数。你可以使用几个毁掉函数来配置一个自定义输入源。核心基础调用在不同点上调用这些函数来配置来源,处理任何传入的事件,在被运行循环移除时拆除来源。

除了当事件发生时定义自定义来源行为,你还必须定义事件交付机制。这部分来源运行在一个单独的线程并负责提供输入源数据,在数据准备处理时用信号通知。事件交付机制由你决定但不会过于复杂。

如何创建一个自定义输入源的例子,参见定义一个自定义输入源(Defining a Custom Input Source)。自定义输入源参考信息,参见CFRunLoopSource 参考(CFRunLoopSource Reference)。

Cocoa执行选择器来源

除了基于端口来源,Cocoa定义自定义输入源,允许你在任何线程上执行一个选择器。类似基于端口来源,执行选择器需要在目标线程上进行序列化,减轻在一个线程上运行的多个方法可能发生的同步问题。与基于端口来源不同的是,执行选择器来源在执行完选择器后会从运行循环上移除自己。

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

当在另一个线程上执行一个选择器,目标线程必须有一个活跃的运行循环。对于你创建的线程,这意味着等待直到你代码显式的启动运行循环。因为主线程开始它自己的运行循环,然而,只要应用调用应用代理的applicationDidFinishLaunching: 方法,你可以开始在该线程上调用。运行循环处理所有排队执行选择器,在每次循环调用,而不是在每个循环迭代处理一个。

表3-2 列出NSObject 定义的方法,可用于执行其他线程上的选择器。因为NSObject 声明这些方法,你可以从任何可访问对象的 Objective-C线程,包括POSIX线程来使用它们。这些方法并不创建一个新线程来执行选择器。

表3-2 在其他线程上执行选择器

方法

说明

performSelectorOnMainThread:withObject:waitUntilDone:

performSelectorOnMainThread:withObject:waitUntilDone:modes:

在线程的下个运行循环周期中,执行在应用主线程上的指定选择器。这个方法给你阻塞当前线程的选择直到选择器被执行。

performSelector:onThread:withObject:waitUntilDone:

performSelector:onThread:withObject:waitUntilDone:modes:

在有NSThread 对象的线程上执行指定选择器。执行方法给你阻塞当前线程的选择直到选择器被执行。

performSelector:withObject:afterDelay:

performSelector:withObject:afterDelay:inModes:

在下个运行循环周期和可选延迟周期中,执行当前线程上的指定选择器。因为它将等待下个运行循环周期来执行选择器,这些方法提供一个从当前执行代码自动迷你延迟。多队列选择器一个接一个排队执行。

cancelPreviousPerformRequestsWithTarget:

cancelPreviousPerformRequestsWithTarget:selector:object:

使用performSelector:withObject:afterDelay: 或performSelector:withObject:afterDelay:inModes:方法取消发送消息到当前线程。

关于这些方法的详细信息,参见NSObject 类参考(NSObject Class Reference)。

计时器来源

在未来某个预设的时间,计时器来源同步交付事件到你的线程。计时器是线程通知自己做点事情的一种方式。例如,搜索字段可以使用计时器在用户输入关键字一定时间过去后启动一个自动搜索。使用这种延迟时间让用户在开始搜索前有更多的机会来输入搜索字符串。

虽然它产生自基于时间的通知,一个计时器不是一个实时机制。像输入源,计时器与你运行循环的特定模式有关。如果一个计时器不是当前运行循环监控的模式,它不触发直到你运行循环在计时器支持的一个模式上。同样的,如果在运行循环在执行处理程序过程中计时器触发,计时器会等待直到下次运行循环调用处理程序。如果运行循环不允许,计时器不会触发。

你可以配置计时器来一次或多次重复产生事件。一个重复计时器基于预定触发时间自动重新安排,而非实际触发时间。例如,如果计时器在特定时间后每隔5秒触发,预定触发时间会落在原来5秒间隔上,即使实际触发时间呗延迟。如果触发时间被延迟太多以至错过一个或多个预定触发时间,计时器在错过的那段时间内只触发一次。在错过时间内触发后,计时器按照下个预定触发时间重新安排。

关于配置计时器来源的更多信息,参见配置计时器来源(Configuring Timer Sources)。关于参考信息,参见NSTimer 类参考(NSTimer Class Reference)或CFRunLoopTimer 参考(CFRunLoopTimer Reference

运行循环观察者

与来源相比,在适当的同步或异步事件发生时触发,在执行运行循环本身执行中,运行循环观察者在特定地点触发。你可以使用运行循环观察者让你的线程准备处理一个给定事件或在线程进入休眠状态前准备。你可以将运行循环观察者和运行循环中的以下事件结合起来:

  • 运行循环入口。
  • 当运行循环开始处理计时器。
  • 当运行循环开始处理输入源。
  • 当运行循环开始进入休眠。
  • 当运行循环被唤醒,在处理事件前将其唤醒。
  • 退出运行循环。

你可以使用核心基础往应用中添加运行循环观察者。为了创建一个运行循环观察者,你可以创建一个CFRunLoopObserverRef 不透明类型的新实例。该类型跟踪你自定义回调函数和感兴趣的活动。

类似于计时器,运行循环观察者可以一次或多次使用。一个一次性观察者在它触发后从运行循环上移除自己,然而一个重复观察者仍然连接。当你创建观察者时可以指定该观察者是运行一次还是多次。

如何创建一个运行循环观察者的例子,参见配置运行循环(Configuring the Run Loop)。关于参考信息,参见CFRunLoopObserver 参考(CFRunLoopObserver Reference)。

事件的运行循环序列

每次你运行,线程的运行循环处理等待事件并生成通知附加观察者。顺序如下:

  • 通知观察者已进入运行循环。
  • 通知观察者任何准备的计时器将要触发。
  • 通知观察者任何不基于端口的输入源将要触发。
  • 触发任何不基于端口准备触发的输入源。
  • 如果基于端口输入源准备就绪等待触发,立即处理事件。跳转到第9步。
  • 通知观察者线程将要休眠。
  • 让线程休眠直到以下事件发生:

    • 一个事件到达基于端口的输入源。
    • 计时器触发。
    • 为运行循环到期设置的超时值。
    • 运行循环显式的唤醒
  • 通知观察者线程唤醒
  • 处理等待事件
    • 如果一个用户定义的计时器触发,处理计时器事件并重新启动循环。跳转到步骤2.
    • 如果一个输入源触发,交付事件。
    • 如果运行循环显式的唤醒但尚未超时,重新启动循环,跳转到步骤2.
  • 通知观察者运行循环已退出。

因为计时器和输入源的观察者通知在事件发生前被交付,可能通知时间与实际发生的时间有差距。如果这些事件间的时间至关重要,你可以使用休眠和从休眠到唤醒的通知来帮助你关联实际事件间的时间。

因为计时器和其他定期事件在你运行运行循环时被交付,注意循环会破坏这些事件的交付。当你通过输入一个循环并向应用多次请求事件来实现一个鼠标跟踪程序时,会发生这种行为。因为你的代码直接抓住事件,而非让应用正常调度这些事件,活动的计时器可能不能被触发知道你的鼠标跟踪程序退出并返回让应用控制。

一个运行循环可以用运行循环对象显式的唤醒。其他活动也可能导致运行循环被唤醒。例如,添加另一个非基于端口的输入源来唤醒运行循环,这样可以立即处理输入源,而不是等到其他事件发生。

什么时候可以使用一个运行循环?

当你为你的应用创建次要线程时,是你唯一一次需要显式的运行一个运行循环。应用主线程的运行循环是一个重要的基础设施。因此,应用框架提供代码来运行主要应用循环并自动启动循环。iOSUIApplication 的run 方法(或者OS X中的NSApplication )启动一个应用的主循环作为正常启动序列的一部分。如果你使用Xcode项目模板来创建你的应用,你不应该显式的调用这些程序。

对于次要线程,你需要决定运行循环是否必要的,如果是,你自己配置并启用它。你不需要在所有情况下启动一个线程的运行循环。例如,如果你使用一个线程来执行一些长期和预定的任务,你就能避免启动运行循环。运行循环用于你想与线程有更多交互的情况。例如,你需要启动一个运行循环如果你计划做以下事情:

  • 使用端口或自定义输入源来与其他线程通信。
  • 在线程上使用计时器。
  • 在Cocoa用于中使用任何performSelector方法。
  • 使线程执行周期任务

如果你选择使用运行循环,配置和设置是很简单的。与所有线程编程一样,你应该有个计划在适当的情况下退出次要线程。让它退出比强迫它终止可以更好的更干净的结束线程。如何配置信息和退出运行循环的信息参见使用运行循环对象( Using Run Loop Objects)。

使用运行循环对象

运行循环对象为主要界面提供添加输入源、计时器和运行循环观察者到你的运行循环并运行之。每个线程都有一个单独的运行循环对象与之相关联。在Cocoa,这个对象是NSRunLoop 类的一个实例。在底层应用中,它是一个指向CFRunLoopRef 不透明类型的指针。

获取运行循环对象

为了获取当前线程的运行循环,你可以使用下列方法之一:

虽然他们不是免费桥接类型,当需要时,你可以从NSRunLoop 对象获取一个CFRunLoopRef 不透明类型。NSRunLoop 类定义了一个getCFRunLoop 方法,返回一个CFRunLoopRef 类型,你可以传递到核心基础程序。因为两个对象引用相同运行循环,如果需要你可以混合调用NSRunLoop 对象和CFRunLoopRef 不透明类型。

配置运行循环

在次要线程运行一个运行循环之前,你必须添加至少一个输入源或计时器到运行循环上。如果一个运行循环没有任何来源要监控,当你试图运行它时,它会立即退出。如何往运行循环上添加来源的例子,见配置运行循环来源(Configuring Run Loop Sources)。

除了安装来源,你可以安装运行循环观察者并使用它们来监测运行循环的不同执行阶段。为了安装一个运行循环观察者,创建一个CFRunLoopObserverRef 不透明类型并使用CFRunLoopAddObserver 函数来添加到你的运行循环上。运行循环观察者必须使用核心基础来创建,即便是Cocoa应用。

清单3-1展示了线程的主要程序添加到运行循环的运行循环观察者上。例子的目的是向你展示如何创建运行循环观察者,因此代码简单的设置了一个运行循环来监视所有运行循环活动。基本处理程序(未展示)简单的记录运行循环活动,因为它处理计时器请求。

清单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);
}
当为一个长期线程配置运行循环,最好添加至少一个输入源来接收消息。尽管你可以仅添加一个计时器来进入运行新,一旦计时器触发后,它通常是无效的,这将导致运行循环退出。附加一个重复的计时器可以保持运行循环运行一段较长的时间,但会涉及到周期性的触发计时器唤醒你的线程,这实际上是另一种形式的轮询。相比之下,一个输入源等待事件发生,保持你的线程休眠知道它完成。

启动运行循环

只有你应用中的次要线程才需要启动运行循环。一个运行循环必须至少有一个输入源或计时器用于监控。如果没有,运行循环将立即退出。

有以下几种方法来启动运行循环:

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

无条件的进入你的运行循环是最简单的选择,但也是最不可取的。无条件的运行你的运行循环将线程放置到一个永久循环,你对运行循环本身只有很少的控制。你可以添加和删除输入源和计时器,但停止运行循环的唯一方法是杀死它。也没有一个办法来自定义模式下运行运行循环。

最好是运行一个有超时值的运行循环,而不是无条件的允许。当你使用一个超时值时,运行循环运行知道一个事件到达或者分配的时间过期。如果一个事件到达,该事件被分配到一个处理程序来处理,运行循环退出。你的代码可以重新启动运行循环来处理下一个事件。如果分配的时间过期,你可以简单的重启运行循环或使用该时间来做任何家务。

除了一个超时值,你也可以使用特定模式来运行你的运行循环。模式和超时值并不互相排斥,在启动运行循环时都可以使用。模式限制交付到运行循环事件的来源类型,详情见运行循环模式(Run Loop Modes)。

清单3-2 展示了线程主要入口程序的骨架版本。这个例子的关键部分展示了运行循环的基本结构。从本质上说,你添加输入源和计时器到运行循环,然后反复调用程序来启动运行循环。每次运行循环程序返回,检查是否出现任何条件批准线程退出。例子使用核心基础运行循环程序,这样可以检查返回结果并确定运行循环退出的原因。你也可以使用NSRunLoop 类的方法以相同的方式来运行运行循环,如果你使用Cocoa则不需要检查返回值。(调用NSRunLoop 类方法的运行循环例子,见清单3-14.

清单3-2 运行一个运行循环

- (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.
}

可以递归的运行一个运行循环。换句话说,你可以调用CFRunLoopRunCFRunLoopRunInMode或任何NSRunLoop 方法来启动运行循环,在其中有输入源或计时器的处理程序。当这样做时,你可以使用任何你想要的方式来运行嵌套的运行循环,包括在运行循环外使用的模式。

退出运行循环

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

  • 配置运行循环一个超时值。
  • 告知运行循环停止。

使用一个超时值当然是首选,如果你可以管理它。指定一个超时值,让运行循环完成所有正常进程,包括在退出前交付通知到运行循环观察者。

显式的使用CFRunLoopStop 函数停止运行循环产生的结果类似于超时。运行循环发送任何剩余运行循环通知然后退出。不同的是你可以在无条件启动运行循环时使用此技术。

尽管删除运行循环的输入源和计时器也可能导致运行循环退出,这并不是一个可靠的方法来停止运行新。一些系统程序添加输入源到运行循环来处理事件。因为你的代码不可能意识到这些输入源,它们将无法删除这些,这将阻止运行循环退出。

线程安全和运行循环对象

线程安全变化取决于你使用哪个API来操作你的运行循环。核心基础中的函数通常都是线程安全的,可以从任何线程调用。如果你执行操作来改变运行循环的配置,然而,对于拥有运行循环的线程,这样做仍然是最佳实践。

Cocoa NSRunLoop 类不像核心基础副本本身有线程安全。如果你使用NSRunLoop 类来修改运行循环,只有自己拥有运行循环的线程你才能这么做。添加一个输入源或计时器到属于不同线程的运行循环上可能导致代码崩溃或一种意想不到的行为。

配置运行循环来源

以下章节展示了在Cocoa和核心基础中如何设置不同类型的输入源的例子

定义一个自定义输入源

创建一个自定义输入源包括定义如下内容:

  • 你希望你输入源处理的信息。
  • 调度程序让感兴趣的客户端知道如何接触到你的输入源。
  • 一个处理程序来执行任何客户端发送的请求。
  • 取消程序使你的输入源无效。

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

图3-2 展示了配置自定义输入源的例子。在这个例子中,应用的主线程维护输入源的引用,输入源的自定义命令缓冲区和输入源安装的运行循环。当主线程上有个任务,希望可以切换到工作线程,它布置一个命令到命令缓冲区以及工作线程启动该任务所需的其他信息。(因为主线程和工作线程的输入源可以访问命令缓冲区,访问必须同步的。)一旦发布命令,主线程发信号到输入源并唤醒工作线程的运行循环。接收到唤醒命令,运行循环调用输入源的处理程序,该处理程序处理命令缓冲区中的命令。

3-2 操作一个自定义输入源

 技术分享

以下章节从前面的图解释了自定义输入源的实现,并展示了你需要实现的关键代码。

定义输入源

定义一个自定义输入源需要使用核心基础程序来配置你的运行循环来源并附加到运行循环上。尽管基本处理程序是基于C的函数,但这并不妨碍你为这些函数写包装函数并使用Objective-C或C++来实现代码的主体。

图3-2中介绍的输入源使用一个Objective-C对象来管理命令缓冲区并与运行循环协调。清单3-3 展示了该对象的定义。RunLoopSource 对象管理命令缓冲区,并使用缓冲区来接收其他线程的消息。这个清单也展示了RunLoopContext 对象的定义,该对象只是一个容器对象用于传递RunLoopSource 对象和应用主线程引用的运行循环。

清单3-3 自定义输入源对象定义

@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 对象的信息来完成。

清单3-4 调度运行循环来源

@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


当你输入源被发送信号时,用于处理自定义数据的回调程序是最重要的一个。清单3-5展示了执行与RunLoopSource 对象相关的回调程序。该函数简单的将请求转发给sourceFired 方法,然后处理命令缓冲区中的命令。

清单3-5 在输入源执行工作

void RunLoopSourcePerformRoutine (void *info)
{
    RunLoopSource*  obj = (RunLoopSource*)info;
    [obj sourceFired];
}


如果使用CFRunLoopSourceInvalidate 函数将输入源从运行循环删除,系统调用你的输入源取消程序。你可以使用这个程序来通知客户端你的输入源不再有效,并且它们应该删除有关的引用。清单3-6展示了注册为RunLoopSource 对象的取消回调程序。该函数发送另一个RunLoopContext 对象到应用代理,但这次访问代理是为了删除运行循环来源的引用。

清单3-6 作废输入源

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];
}


注意:应用代理的registerSource: 和removeSource: 方法的代码见与客户端协调输入源(Coordinating with Clients of the Input Source)。

在运行循环上安装输入源

清单3-7 展示了RunLoopSource 类的init 和addToCurrentRunLoop 方法。init 方法创建CFRunLoopSourceRef 不透明对象,必须附加到运行循环。它传递RunLoopSource 对象本身作为环境信息,以便回调程序有一个指向该对象的指针。输入源的安装不会发生直到工作线程调用addToCurrentRunLoop 方法,该方法指向RunLoopSourceScheduleRoutine 回调函数。一旦输入源添加到运行循环上,线程可以运行它的运行循环。

清单3-7 安装运行循环来源

- (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 对象调度函数被调用时调用。该方法接收RunLoopSource 对象提供的RunLoopContext 对象,并将其添加到它的来源列表。该清单还展示了当输入源从运行循环上删除时,用于注销输入源的程序。

清单3-8 注册和删除应用代理的输入源

- (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展示了RunLoopSource 对象的fireCommandsOnRunLoop 方法。当客户端准备处理添加到缓冲区的命令,客户端调用这个方法。

清单3-9 唤醒运行循环

- (void)fireCommandsOnRunLoop:(CFRunLoopRef)runloop
{
    CFRunLoopSourceSignal(runLoopSource);
    CFRunLoopWakeUp(runloop);
}

注意:你不应该通过消息传递一个自定义输入源,尝试处理一个SIGHUP 或其他类型的流程级信号。为唤醒运行循环的核心基础框架函数不是信号安全的,不应该用于你的应用信号处理程序内部。关于信号处理程序的更多信息,参见sigaction 操作说明。

配置计时器来源

为了创建一个计时器来源,所有你需要做的是创建一个计时器对象并安排其到你的运行循环上。在Cocoa中,你使用NSTimer 类来创建新计时器对象,在核心基础中你使用CFRunLoopTimerRef 不透明对象。在内部,NSTimer 类是核心框架的一个扩展,提供了一些便利的特性,可以使用相同的方法来创建和安排计时器。

在Cocoa中,你可以使用以下其中一种方法来一次性创建和安排计时器:

这些方法创建计时器并以默认模式(NSDefaultRunLoopMode)将其添加到当前线程的运行循环上。你也可以手动的安排一个计时器如果你希望通过创建你的NSTimer 对象然后使用NSRunLoop的 addTimer:forMode: 方法将其添加到运行循环上。两种技术基本上做同样的事情,但给你不同层次的控制计时器的配置。例如,如果你创建计时器并手动的将其添加到运行循环上,你可以使用其他模式而不是默认模式。清单3-10 展示了如何使用这两种技术来创建计时器。第一个计时器初始延迟1秒,但有规律的每0.1秒触发。第二个计时器在最初0.2秒的延迟后开始触发,然后每0.2秒触发一次。

清单3-10 使用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展示了使用核心基础函数配置一个计时器需要使用的代码。尽管这个例子不传递环境结构中任何用户定义的信息,你可以使用这个结构传递任何计时器需要的自定义数据。关于这种结构的更多信息,参见CFRunLoopTimer 参考(CFRunLoopTimer Reference)。

清单3-11 使用核心基础创建和安排一个计时器

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和核心基础提供基于端口对象来完成线程或进程间的通信。以下章节展示了如何使用集中不同类型的端口来设置端口通信。

配置一个NSMachPort 对象

为了建立与NSMachPort 对象的本地连接,创建端口对象并将其添加到你的主要线程的运行循环上。当启动次要线程时,传递相同的对象到你线程的入口点函数。次要线程可以使用相同的对象来发送消息到你的主要线程。

实现主线程代码

清单3-12 展示了启动次要工作线程的主要线程代码。因为Cocoa框架执行了配置端口和运行循环的许多干预步骤,launchThread 方法明显比核心基础中等效方法(清单3-17)短,然而,这两者的行为几乎是相同的。一个区别是,该方法直接发送NSPort 对象,而非发送工作线程的本地端口的名称。

清单3-12 主线程的启动方法

- (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];
    }
}

为了建立线程间的双向通信信道,你可能希望工作线程以登记消息发送自己本地端口到你的主线程。接收到登记消息使你的主线程知道启动次要线程一切顺利,也为你提供一种方法来发送进一步消息到另一个线程。

清单3-13 展示了主线程的handlePortMessage:方法。当数据到达线程的本地端口时调用该方法。当登记消息到达时,该方法直接从端口消息检索端口用于次要线程,并保存以供以后使用。

清单3-13 处理Mach端口消息

#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.
    }
}


实现次要线程代码

对于次要工作线程,你必须配置线程并使用指定端口将通信信息返回到主线程。

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

清单3-14 使用Mach端口启动工作线程

+(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 展示了次要线程的登记程序。该方法设置自己本地端口用于未来通信,并发送一个登记信息返回到主线程。该方法使用LaunchThreadWithPort: 方法接收到的端口对象作为消息的目标。

清单3-15 使用Mach端口发送登记消息

// 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 展示了你希望使用消息端口情况下端口的创建和注册过程。

清单3-16 注册消息端口

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];


在核心基础中配置一个基于端口的输入源

以下章节展示了如何使用核心基础来设置应用主线程与工作线程间的双向通信信道。

清单3-17 展示了应用主线程启动工作线程的代码调用。代码做的第一件事情是建立一个CFMessagePortRef 不透明类型从工作线程监听消息。工作线程需要端口的名称来进行连接,以便字符串交付到工作线程的入口函数。端口名称通常在当前用户环境是唯一的,否则,你可能遇到冲突。

清单3-17 附加核心基础消息端口到一个新线程

#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 函数,见清单3-18。该函数提取端口名称用于工作线程并创建一个管道用于未来通信。

清单3-18 接收登记信息

#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;
}

配置好主线程,剩下唯一的事情就是为新创建的工作线程创建自己的端口和登记。清单3-19展示了工作线程的入口函数。该函数提取主线程的端口名称并使用它来创建一个远程连接返回到主线程。该函数创建一个本地端口,安装端口到线程的运行循环,并将登记消息包括本地端口名称发送到主线程。

清单3-19 设置线程结构

#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;
}

一旦它进入运行循环,所有未来事件发送到线程端口由ProcessClientRequest 函数处理。该函数的实现取决于线程完成工作的种类,在这里并没有展示。

 

官方原文地址:

https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW1 


运行循环(Run Loops)

标签:run loops   thread   timer   input source   thread safety   

原文地址:http://blog.csdn.net/iosswift/article/details/43939547

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!