- 小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。
目录如下:
- iOS 底层原理探索 之 alloc
- iOS 底层原理探索 之 结构体内存对齐
- iOS 底层原理探索 之 对象的本质 & isa的底层实现
- iOS 底层原理探索 之 isa - 类的底层原理结构(上)
- iOS 底层原理探索 之 isa - 类的底层原理结构(中)
- iOS 底层原理探索 之 isa - 类的底层原理结构(下)
- iOS 底层原理探索 之 Runtime运行时&方法的本质
- iOS 底层原理探索 之 objc_msgSend
- iOS 底层原理探索 之 Runtime运行时慢速查找流程
- iOS 底层原理探索 之 动态方法决议
- iOS 底层原理探索 之 消息转发流程
- iOS 底层原理探索 之 应用程序加载原理dyld (上)
- iOS 底层原理探索 之 应用程序加载原理dyld (下)
- iOS 底层原理探索 之 类的加载
- iOS 底层原理探索 之 分类的加载
- iOS 底层原理探索 之 关联对象
- iOS底层原理探索 之 魔法师KVC
- iOS底层原理探索 之 KVO原理|8月更文挑战
- iOS底层原理探索 之 重写KVO|8月更文挑战
- iOS底层原理探索 之 多线程原理|8月更文挑战
- iOS底层原理探索 之 GCD函数和队列
- iOS底层原理探索 之 GCD原理(上)
- iOS底层 - 关于死锁,你了解多少?
- iOS底层 - 单例 销毁 可否 ?
- iOS底层 - Dispatch Source
- iOS底层 - 一个栅栏函 拦住了 数
- iOS底层 - 不见不散 的 信号量
- iOS底层 GCD - 一进一出 便成 调度组
- iOS底层原理探索 - 锁的基本使用
- iOS底层 - @synchronized 流程分析
- iOS底层 - 锁的原理探索
- iOS底层 - 带你实现一个读写锁
- iOS底层 - 谈Objective-C block的实现(上)
- iOS底层 - 谈Objective-C block的实现(下)
- iOS底层 - Block, 全面解析!
- iOS底层 - 启动优化(上)
- iOS底层 - 启动优化(下)
- iOS底层原理探索 -- 内存管理 之 内存五大区
- iOS底层原理探索 -- 内存管理 之 Tagged Pointer Format Changes
- iOS底层原理探索 -- 内存管理 之 retain & release
- iOS底层原理探索 -- 内存管理 之 弱引用表
- iOS底层原理探索 -- 内存管理 之 @autoreleasepool
以上内容的总结专栏
细枝末节整理
前言
作为开发者,我们的项目安装到设备之后,可以在用户打开的时候一直运行着,用户点击了就响应用户的点击事件; 我们有定时的任务需要处理,到时间了,系统就可以去处理我们的任务;整个过程中,在正常情况下CPU的占用也十分的友好,那么,系统是如何做到这一点呢?看完今天的内容,我想,你会有一个新的理解。话不多说,这就开始今天的内容。
RunLoop
运行循环是与线程相关的基础架构的一部分。一个运行循环是一个事件处理循环,你用它来安排工作,并协调接收传入的事件。运行循环的目的是在有工作要做的时候让你的线程保持忙碌,在没有工作时让你的线程休眠。
运行循环管理不是完全自动的。您仍然必须设计线程的代码以在适当的时间启动运行循环并响应传入的事件。Cocoa 和 Core Foundation 都提供运行循环对象来帮助您配置和管理线程的运行循环。您的应用程序不需要显式创建这些对象;每个线程,包括应用程序的主线程,都有一个关联的运行循环对象。然而,只有辅助线程需要显式运行它们的运行循环。作为应用程序启动过程的一部分,应用程序框架会在主线程上自动设置和运行运行循环。
RunLoop 解析
运行循环非常像它的名字。它是您的线程进入并用于运行事件处理程序以响应传入事件的循环。你的代码提供了用于实现 run loop 的实际循环部分的控制语句——换句话说,你的代码提供了驱动 run loop的while
or for
循环。在您的循环中,您使用运行循环对象来“运行”接收事件并调用已安装处理程序的事件处理代码。
运行循环从两种不同类型的源接收事件。输入源传递异步事件,通常是来自另一个线程或不同应用程序的消息。定时器源提供同步事件,在预定时间或重复间隔发生。两种类型的源都使用特定于应用程序的处理程序例程来处理到达的事件。
图 3-1显示了运行循环和各种源的概念结构。输入源将异步事件传递给相应的处理程序并导致runUntilDate:
方法(在线程的关联NSRunLoop
对象上调用)退出。定时器源将事件传递给它们的处理程序例程,但不会导致运行循环退出。
除了处理输入源之外,运行循环还会生成有关运行循环行为的通知。注册的run-loop 观察者可以接收这些通知并使用它们对线程进行额外的处理。您使用 Core Foundation 在线程上安装运行循环观察器。
以下部分提供了有关运行循环组件及其运行模式的更多信息。它们还描述了在处理事件期间在不同时间生成的通知。
运行循环模式
一个运行的循环模式是输入源和定时器的集合进行监测和运行循环观察员集合通知。每次运行运行循环时,您都指定(显式或隐式)运行的特定“模式”。在运行循环的那段过程中,只有与该模式关联的源才会被监视并允许传递它们的事件。(类似地,只有与该模式关联的观察者才会被通知运行循环的进度。)与其他模式关联的源会保留任何新事件,直到随后以适当的模式通过循环。
在您的代码中,您可以通过名称识别模式。Cocoa 和 Core Foundation 都定义了默认模式和几种常用模式,以及用于在代码中指定这些模式的字符串。您可以通过简单地为模式名称指定自定义字符串来定义自定义模式。尽管您分配给自定义模式的名称是任意的,但这些模式的内容并非如此。您必须确保将一个或多个输入源、计时器或运行循环观察器添加到您创建的任何模式中,以便它们有用。
在运行循环的特定传递期间,您可以使用模式从不需要的源中过滤掉事件。大多数时候,你会希望在系统定义的“默认”模式下运行你的运行循环。但是,模态面板可能会在“模态”模式下运行。在这种模式下,只有与模态面板相关的源才会将事件传递给线程。对于辅助线程,您可以使用自定义模式来防止低优先级源在时间关键操作期间传递事件。
注意: 模式根据事件的来源而不是事件的类型进行区分。例如,您不会使用模式只匹配鼠标按下事件或键盘事件。您可以使用模式来侦听一组不同的端口、暂时挂起计时器或以其他方式更改当前正在监视的源和运行循环观察者。
表 3-1列出了 Cocoa 和 Core Foundation 定义的标准模式以及何时使用该模式的描述。名称列列出了用于在代码中指定模式的实际常量。
Mode | Name | Description |
---|---|---|
Default | NSDefaultRunLoopMode (Cocoa)kCFRunLoopDefaultMode (Core Foundation) | 默认模式是用于大多数操作的模式。大多数情况下,您应该使用此模式来启动您的运行循环并配置您的输入源。 |
Connection | NSConnectionReplyMode (Cocoa) | Cocoa 将此模式与NSConnection 对象结合使用来监视回复。您应该很少需要自己使用这种模式。 |
Modal | NSModalPanelRunLoopMode (Cocoa) | Cocoa 使用这种模式来识别用于模态面板的事件。 |
Event tracking | NSEventTrackingRunLoopMode (Cocoa) | Cocoa 使用此模式在鼠标拖动循环和其他类型的用户界面跟踪循环期间限制传入事件。 |
Common modes | NSRunLoopCommonModes (Cocoa)kCFRunLoopCommonModes (Core Foundation) | 这是一组可配置的常用模式。将输入源与此模式相关联也会将其与组中的每个模式相关联。对于 Cocoa 应用程序,该集合默认包括默认、模态和事件跟踪模式。Core Foundation 最初仅包含默认模式。您可以使用该CFRunLoopAddCommonMode 功能将自定义模式添加到集合中。 |
输入源
输入源将事件异步传送到您的线程。事件的来源取决于输入源的类型,通常是两个类别之一。基于端口的输入源监视应用程序的 Mach 端口。自定义输入源监视自定义事件源。就您的运行循环而言,输入源是基于端口的还是自定义的并不重要。系统通常会实现您可以按原样使用的两种类型的输入源。两个来源之间的唯一区别是它们是如何发出信号的。基于端口的源由内核自动发出信号,自定义源必须从另一个线程手动发出信号。
创建输入源时,将其分配给运行循环的一种或多种模式。模式影响在任何给定时刻监视哪些输入源。大多数情况下,您在默认模式下运行 run loop,但您也可以指定自定义模式。如果输入源不在当前监控模式下,它生成的任何事件都会被保留,直到运行循环以正确的模式运行。
以下部分描述了一些输入源。
基于端口的源
Cocoa 和 Core Foundation 为使用端口相关的对象和函数创建基于端口的输入源提供内置支持。例如,在 Cocoa 中,您根本不必直接创建输入源。您只需创建一个端口对象并使用 的方法NSPort
将该端口添加到运行循环中。端口对象为您处理所需输入源的创建和配置。
在 Core Foundation 中,您必须手动创建端口及其运行循环源。在这两种情况下,您使用的端口类型不透明(相关的功能CFMachPortRef
,CFMessagePortRef
或CFSocketRef
)创建合适的对象。
有关如何设置和配置自定义基于端口的源的示例,请参阅配置基于端口的输入源。
自定义输入源
要创建自定义输入源,您必须使用与CFRunLoopSourceRef
Core Foundation 中的opaque 类型关联的函数。您可以使用多个回调函数配置自定义输入源。Core Foundation 在不同的点调用这些函数来配置源,处理任何传入的事件,并在源从运行循环中删除时拆除源。
除了定义事件到达时自定义源的行为之外,您还必须定义事件传递机制。源的这部分运行在一个单独的线程上,负责向输入源提供其数据,并在该数据准备好处理时发出信号。事件传递机制由您决定,但不必过于复杂。
有关如何创建自定义输入源的示例,请参阅定义自定义输入源。有关自定义输入源的参考信息,另请参阅*CFRunLoopSource 参考*。
Cocoa Perform 选择器源
除了基于端口的源之外,Cocoa 还定义了一个自定义输入源,允许您在任何线程上执行选择器。与基于端口的源一样,执行选择器请求在目标线程上被序列化,从而缓解了在一个线程上运行多个方法时可能出现的许多同步问题。与基于端口的源不同,执行选择器源在执行其选择器后将自身从运行循环中移除。
注意: 在 OS X v10.5 之前,执行选择器源主要用于向主线程发送消息,但在 OS X v10.5 及更高版本和 iOS 中,您可以使用它们向任何线程发送消息。
在另一个线程上执行选择器时,目标线程必须有一个活动的运行循环。对于您创建的线程,这意味着等待您的代码显式启动运行循环。但是,因为主线程启动了它自己的运行循环,所以一旦应用程序调用applicationDidFinishLaunching:
应用程序委托的方法,您就可以开始对该线程发出调用 。运行循环每次通过循环处理所有排队的执行选择器调用,而不是在每次循环迭代期间处理一个。
表 3-2列出了NSObject
可用于在其他线程上执行选择器的方法。因为这些方法是在 上声明的NSObject
,所以您可以在任何可以访问 Objective-C 对象的线程中使用它们,包括 POSIX 线程。这些方法实际上并没有创建一个新线程来执行选择器。
方法 | 描述 |
---|---|
performSelectorOnMainThread:withObject:waitUntilDone:``performSelectorOnMainThread:withObject:waitUntilDone:modes: | 在该线程的下一个运行循环周期期间,在应用程序的主线程上执行指定的选择器。这些方法使您可以选择在执行选择器之前阻塞当前线程。 |
performSelector:onThread:withObject:waitUntilDone:``performSelector:onThread:withObject:waitUntilDone:modes: | 在您拥有NSThread 对象的任何线程上执行指定的选择器。这些方法使您可以选择在执行选择器之前阻塞当前线程。 |
performSelector:withObject:afterDelay:``performSelector:withObject:afterDelay:inModes: | 在下一个运行循环周期和可选的延迟期之后,在当前线程上执行指定的选择器。因为它会等到下一个运行循环循环来执行选择器,所以这些方法从当前执行的代码提供了一个自动的最小延迟。多个排队的选择器按照它们排队的顺序一个接一个地执行。 |
cancelPreviousPerformRequestsWithTarget:``cancelPreviousPerformRequestsWithTarget:selector:object: | 允许您使用performSelector:withObject:afterDelay: orperformSelector:withObject:afterDelay:inModes: 方法取消发送到当前线程的消息。 |
有关这些方法的详细信息,请参阅*NSObject 类参考*。
定时器源
定时器源在未来的预设时间将事件同步传送到您的线程。定时器是线程通知自己做某事的一种方式。例如,一旦在用户连续击键之间经过了一定时间,搜索字段可以使用计时器来启动自动搜索。使用此延迟时间使用户有机会在开始搜索之前键入尽可能多的所需搜索字符串。
尽管它生成基于时间的通知,但计时器不是实时机制。与输入源一样,计时器与运行循环的特定模式相关联。如果计时器不在运行循环当前监视的模式中,则在您以计时器支持的模式之一运行运行循环之前,它不会触发。类似地,如果在 run loop 正在执行处理程序例程时触发计时器,则计时器将等待直到下一次通过 run loop 调用其处理程序例程。如果 run loop 根本没有运行,则计时器永远不会触发。
您可以将计时器配置为仅生成一次或重复生成事件。重复计时器根据计划的触发时间而不是实际的触发时间自动重新安排自身。例如,如果计时器计划在特定时间触发,并且在此之后每 5 秒触发一次,则计划触发时间将始终落在原始的 5 秒时间间隔内,即使实际触发时间被延迟。如果触发时间延迟太多以至于错过了一个或多个预定的触发时间,则计时器在错过的时间段内仅触发一次。在错过的时间段触发后,定时器被重新安排到下一个预定的触发时间。
有关配置定时器源的更多信息,请参阅配置定时器源。有关参考信息,请参阅*NSTimer 类参考或CFRunLoopTimer 参考*。
运行循环观察者
与在发生适当的异步或同步事件时触发的源相反,运行循环观察者在运行循环本身的执行期间在特殊位置触发。您可以使用运行循环观察者来准备线程以处理给定的事件或在线程进入睡眠状态之前准备线程。您可以将运行循环观察者与运行循环中的以下事件相关联:
- 运行循环的入口。
- 当 run loop 即将处理一个计时器时。
- 当 run loop 即将处理输入源时。
- 当运行循环即将进入睡眠状态时。
- 当 run loop 唤醒时,但在它处理唤醒它的事件之前。
- 退出运行循环。
您可以使用 Core Foundation 向应用程序添加运行循环观察器。要创建运行循环观察者,您需要创建一个CFRunLoopObserverRef
opaque 类型的新实例。这种类型会跟踪您的自定义回调函数及其感兴趣的活动。
与计时器类似,运行循环观察者可以使用一次或重复使用。单次观察者在触发后将自己从运行循环中移除,而重复观察者保持连接状态。您可以在创建观察者时指定观察者是运行一次还是重复运行。
有关如何创建运行循环观察者的示例,请参阅配置运行循环。有关参考信息,请参阅*CFRunLoopObserver 参考*。
事件的运行循环序列
每次运行它时,线程的运行循环都会处理挂起的事件并为任何附加的观察者生成通知。它执行此操作的顺序非常具体,如下所示:
-
通知观察者已经进入运行循环。
-
通知观察者任何准备好的计时器即将触发。
-
通知观察者任何不基于端口的输入源都将被触发。
-
触发任何准备触发的非基于端口的输入源。
-
如果基于端口的输入源已准备好并等待触发,则立即处理该事件。转到步骤 9。
-
通知观察者线程即将休眠。
-
使线程休眠,直到发生以下事件之一:
- 事件到达基于端口的输入源。
- 计时器触发。
- 为运行循环设置的超时值到期。
- 运行循环被显式唤醒。
-
通知观察者线程刚刚醒来。
-
处理挂起的事件。
- 如果触发了用户定义的计时器,则处理计时器事件并重新启动循环。转到步骤 2。
- 如果输入源触发,则传递事件。
- 如果运行循环被显式唤醒但尚未超时,则重新启动循环。转到步骤 2。
-
通知观察者运行循环已经退出。
因为定时器和输入源的观察者通知是在这些事件实际发生之前传递的,所以通知的时间和实际事件的时间之间可能存在差距。如果这些事件之间的时间安排很重要,您可以使用睡眠和从睡眠中唤醒通知来帮助您关联实际事件之间的时间安排。
因为在运行运行循环时会传递计时器和其他周期性事件,所以绕过该循环会中断这些事件的传递。每当您通过进入循环并重复从应用程序请求事件来实现鼠标跟踪例程时,就会出现此行为的典型示例。因为您的代码是直接获取事件,而不是让应用程序正常调度这些事件,所以活动计时器将无法触发,直到您的鼠标跟踪例程退出并将控制权返回给应用程序之后。
可以使用 run loop 对象显式唤醒 run loop。其他事件也可能导致运行循环被唤醒。例如,添加另一个非基于端口的输入源会唤醒运行循环,以便可以立即处理输入源,而不是等待其他事件发生。
什么时候使用 Run Loop?
唯一需要显式运行运行循环的时间是为应用程序创建辅助线程时。应用程序主线程的运行循环是基础设施的关键部分。因此,应用程序框架提供了用于运行主应用程序循环并自动启动该循环的代码。iOS(或OS X)中的run
方法作为正常启动序列的一部分启动应用程序的主循环。如果您使用 Xcode 模板项目来创建应用程序,则永远不必显式调用这些例程。UIApplication``NSApplication
对于二级线程,需要自己判断是否需要run loop,如果需要,自己配置启动。您不需要在所有情况下都启动线程的运行循环。例如,如果你使用一个线程来执行一些长时间运行和预定的任务,你可能可以避免启动运行循环。运行循环适用于您希望与线程进行更多交互的情况。例如,如果您打算执行以下任何操作,则需要启动一个运行循环:
- 使用端口或自定义输入源与其他线程通信。
- 在线程上使用计时器。
performSelector
在 Cocoa 应用程序中使用任何... 方法。- 保持线程执行周期性任务。
如果您确实选择使用运行循环,则配置和设置非常简单。但是,与所有线程编程一样,您应该制定在适当情况下退出辅助线程的计划。通过让线程退出来干净地结束线程总是比强制它终止要好。有关如何配置和退出 run loop 的信息在Using Run Loop Objects 中描述。
使用运行循环对象
一个 run loop 对象提供了一个主界面,用于向你的 run loop 添加输入源、计时器和 run-loop 观察者,然后运行它。每个线程都有一个与之关联的运行循环对象。在 Cocoa 中,这个对象是NSRunLoop
类的一个实例。在低级应用程序中,它是一个指向CFRunLoopRef
不透明类型的指针。
获取运行循环对象
要获取当前线程的运行循环,请使用以下方法之一:
- 在 Cocoa 应用程序中,使用 的
currentRunLoop
类方法NSRunLoop
来检索NSRunLoop
对象。 - 使用该
CFRunLoopGetCurrent
功能。
尽管它们不是免费的桥接类型,但您可以在需要时CFRunLoopRef
从NSRunLoop
对象中获取不透明类型。本NSRunLoop
类定义了一个getCFRunLoop
返回的方法CFRunLoopRef
类型,你可以传递给Core Foundation的例程。因为两个对象都引用同一个运行循环,所以您可以根据需要混合调用NSRunLoop
对象和CFRunLoopRef
不透明类型。
配置运行循环
在辅助线程上运行 run loop 之前,您必须至少向其添加一个输入源或计时器。如果 run loop 没有任何要监控的源,它会在您尝试运行时立即退出。有关如何将源添加到运行循环的示例,请参阅配置运行循环源。
除了安装源代码之外,您还可以安装运行循环观察器并使用它们来检测运行循环的不同执行阶段。要安装运行循环观察器,您需要创建一个CFRunLoopObserverRef
opaque 类型并使用该CFRunLoopAddObserver
函数将其添加到您的运行循环中。运行循环观察者必须使用 Core Foundation 创建,即使对于 Cocoa 应用程序也是如此。
清单 3-1显示了一个线程的主例程,该线程将一个运行循环观察者附加到它的运行循环上。该示例的目的是向您展示如何创建一个 run loop 观察者,因此代码只是设置了一个 run loop 观察者来监视所有 run loop 活动。基本处理程序例程(未显示)只是在处理计时器请求时记录运行循环活动。
- (void)threadMain
{
// 应用程序使用垃圾收集,因此不需要自动释放池。
NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
// 创建一个运行循环观察者并将其附加到运行循环中。
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);
}
// 创建和调度定时器。
[NSTimer scheduledTimerWithTimeInterval:0.1 target:self
selector:@selector(doFireTimer:) userInfo:nil repeats:YES];
NSInteger loopCount = 10;
do
{
// 运行 run loop 10 次让计时器触发。
[myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
loopCount--;
}
while (loopCount);
}
在为长寿命线程配置 run loop 时,最好添加至少一个输入源来接收消息。尽管您可以只连接一个计时器进入运行循环,但是一旦计时器触发,它通常会失效,这将导致运行循环退出。附加一个重复的计时器可以让运行循环运行更长的时间,但会涉及定期触发计时器以唤醒您的线程,这实际上是另一种形式的轮询。相比之下,输入源等待事件发生,让您的线程保持休眠直到它发生。
启动运行循环
只有应用程序中的辅助线程才需要启动运行循环。一个运行循环必须至少有一个输入源或定时器来监控。如果没有附加,则运行循环立即退出。
有几种方法可以启动run loop,包括以下几种:
- 无条件
- 有时间限制
- 在特定模式下
无条件地进入你的运行循环是最简单的选择,但也是最不可取的。无条件地运行你的 run loop 会将线程放入一个永久循环中,这让你对 run loop 本身几乎没有控制。您可以添加和删除输入源和计时器,但停止运行循环的唯一方法是终止它。也没有办法在自定义模式下运行运行循环。
与其无条件地运行 run loop,不如运行带有 timeout 值的 run loop。当您使用超时值时,运行循环会一直运行,直到事件到达或分配的时间到期。如果事件到达,则将该事件分派给处理程序进行处理,然后运行循环退出。然后你的代码可以重新启动运行循环来处理下一个事件。如果分配的时间过期,您可以简单地重新启动运行循环或使用时间来做任何需要的内务处理。
除了超时值,您还可以使用特定模式运行运行循环。模式和超时值不是互斥的,可以在启动运行循环时使用。模式限制了向运行循环传递事件的源类型,在运行循环模式中有更详细的描述。
清单 3-2显示了一个线程的主入口例程的骨架版本。这个例子的关键部分展示了一个运行循环的基本结构。本质上,您将输入源和计时器添加到运行循环中,然后重复调用其中一个例程来启动运行循环。每次运行循环例程返回时,您都会检查是否出现任何可能需要退出线程的条件。该示例使用 Core Foundation 运行循环例程,以便它可以检查返回结果并确定运行循环退出的原因。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.
}
可以递归地运行运行循环。换句话说,您可以从输入源或计时器的处理程序例程中调用CFRunLoopRun、CFRunLoopRunInMode或任何NSRunLoop用于启动运行循环的方法。这样做时,您可以使用任何想要运行嵌套运行循环的模式,包括外部运行循环使用的模式。
退出运行循环
有两种方法可以让运行循环在处理事件之前退出:
- 配置运行循环以使用超时值运行。
- 告诉运行循环停止。
如果您可以管理它,当然首选使用超时值。指定超时值可以让运行循环在退出之前完成其所有正常处理,包括向运行循环观察者发送通知。
使用该CFRunLoopStop
函数显式停止运行循环会产生类似于超时的结果。运行循环发送任何剩余的运行循环通知,然后退出。不同之处在于您可以在无条件启动的运行循环上使用此技术。
虽然移除一个 run loop 的输入源和计时器也可能导致 run loop 退出,但这并不是一个可靠的停止 run loop 的方法。一些系统例程将输入源添加到运行循环以处理所需的事件。因为您的代码可能不知道这些输入源,所以将无法删除它们,这会阻止运行循环退出。
线程安全和运行循环对象
线程安全取决于您用于操作运行循环的 API。Core Foundation 中的函数通常是线程安全的,可以从任何线程调用。但是,如果您正在执行更改 run loop 配置的操作,那么尽可能从拥有 run loop 的线程执行此操作仍然是一个好习惯。
CocoaNSRunLoop
类在本质上不如 Core Foundation 类那样线程安全。如果您正在使用NSRunLoop
该类来修改您的运行循环,您应该只从拥有该运行循环的同一个线程中这样做。将输入源或计时器添加到属于不同线程的运行循环可能会导致您的代码崩溃或以意外方式运行。
配置运行循环源
以下部分展示了如何在 Cocoa 和 Core Foundation 中设置不同类型的输入源的示例。
定义自定义输入源
创建自定义输入源涉及定义以下内容:
- 您希望输入源处理的信息。
- 一个调度程序,让感兴趣的客户知道如何联系您的输入源。
- 执行任何客户端发送的请求的处理程序例程。
- 使您的输入源无效的取消例程。
因为您创建了一个自定义输入源来处理自定义信息,所以实际配置设计得非常灵活。调度程序、处理程序和取消例程是自定义输入源几乎总是需要的关键例程。然而,其余的输入源行为的大部分发生在这些处理程序例程之外。例如,由您定义将数据传递到输入源以及将输入源的存在传达给其他线程的机制。
图 3-2显示了自定义输入源的示例配置。在此示例中,应用程序的主线程维护对输入源、该输入源的自定义命令缓冲区以及安装输入源的运行循环的引用。当主线程有一个任务要交给工作线程时,它会向命令缓冲区发布一个命令以及工作线程启动任务所需的任何信息。(因为主线程和工作线程的输入源都可以访问命令缓冲区,所以访问必须同步。)一旦命令发布,主线程就会向输入源发出信号并唤醒工作线程的运行循环。收到唤醒命令后,运行循环调用输入源的处理程序,该处理程序处理在命令缓冲区中找到的命令。
图 3-2 操作自定义输入源
以下部分解释了上图中自定义输入源的实现,并显示了您需要实现的关键代码。
定义输入源
定义自定义输入源需要使用 Core Foundation 例程来配置运行循环源并将其附加到运行循环。尽管基本处理程序是基于 C 的函数,但这并不妨碍您为这些函数编写包装器并使用 Objective-C 或 C++ 来实现代码主体。
图 3-2 中介绍的输入源使用一个 Objective-C 对象来管理命令缓冲区并与运行循环协调。清单 3-3显示了这个对象的定义。该RunLoopSource
对象管理一个命令缓冲区并使用该缓冲区从其他线程接收消息。此清单还显示了RunLoopContext
对象的定义,它实际上只是一个容器对象,用于将RunLoopSource
对象和运行循环引用传递给应用程序的主线程。
清单 3-3 自定义输入源对象定义
@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调度运行循环源
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
方法,该方法然后由sourceFired处理命令缓冲区中存在的任何命令。
清单3-5在输入源中执行工作
void RunLoopSourcePerformRoutine (void *info)
{
RunLoopSource* obj = (RunLoopSource*)info;
[obj sourceFired];
}
如果您使用CFRunLoopSourceInvalidate
函数从其运行循环中删除输入源,系统将调用输入源的取消例程。您可以使用此例程通知客户端您的输入源不再有效,他们应该删除对它的任何引用。清单3-6显示了在RunLoopSource
对象中注册的取消回调例程。此函数向应用程序委托发送另一个RunLoopContext
对象,但这次要求委托删除对run循环源的引用。
清单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];
}
在运行循环乐段上安装输入源
清单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
对象的调度器函数时调用的注册方法。此方法接收RunLoopContext
对象提供的RunLoopSource
对象,并将其添加到其源列表中。此列表还显示了当输入源从运行循环中删除时用于取消注册的例程。
清单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-9显示了RunLoopSource
对象的fireCommandsOnRunLoop
方法。当客户端准备好让源处理他们添加到缓冲区的命令时,他们会调用此方法。
清单3-9唤醒运行循环
- (void)fireCommandsOnRunLoop:(CFRunLoopRef)runloop
{
CFRunLoopSourceSignal(runLoopSource);
CFRunLoopWakeUp(runloop);
}
配置计时器源
要创建计时器源,您只需创建一个计时器对象,并将其安排在运行循环上。在Cocoa中,您使用NSTimer
类创建新的计时器对象,在Core Foundation中,您使用CFRunLoopTimerRef
不透明类型。在内部,NSTimer
类只是Core Foundation的扩展,它提供了一些方便的功能,例如使用同一方法创建和调度计时器的能力。
在Cocoa中,您可以使用以下类方法之一一次性创建和安排计时器:
scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
scheduledTimerWithTimeInterval:invocation:repeats:
这些方法创建计时器,并在默认模式(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显示了使用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和Core Foundation都为线程之间或进程之间的通信提供了基于端口的对象。以下部分向您展示了如何使用几种不同类型的端口设置端口通信。
配置NSMachPort对象
要与NSMachPort
对象建立本地连接,您需要创建端口对象并将其添加到主线程的运行循环中。启动辅助线程时,您将同一对象传递给线程的入口点函数。辅助线程可以使用同一对象将消息发送回主线程。
实现主线程代码
清单3-12显示了启动辅助辅助线程的主要线程代码。由于Cocoa框架执行了许多配置端口和运行循环的干预步骤,launchThread
方法明显短于其Core Foundation等效物(清单3-17);然而,两者的行为几乎相同。一个区别是,这种方法直接发送NSPort
对象,而不是将本地端口的名称发送到辅助线程。
- (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处理马赫端口消息
#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.
}
}
实现次要线程代码
对于辅助工作线程,您必须配置线程,并使用指定的端口将信息通信回主线程。
Listing 3-14 shows the code for setting up the worker thread. After creating an autorelease pool for the thread, the method creates a worker object to drive the thread execution. The worker object’s sendCheckinMessage:
method (shown in Listing 3-15) creates a local port for the worker thread and sends a check-in message back to the main thread.
清单3-14使用马赫端口启动辅助线程
+(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:
方法中收到的端口对象作为消息的目标。
// 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
对象建立本地连接,您不能简单地在线程之间传递端口对象。远程消息端口必须通过名称获取。要使在可可中实现这一点,需要使用特定名称注册您的本地端口,然后将该名称传递给远程线程,以便它能够获得适当的端口对象进行通信。清单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];
在Core Foundation中配置基于端口的输入源
本节演示如何使用Core Foundation在应用程序的主线程和辅助线程之间设置双向通信通道。
清单3-17显示了应用程序主线程为启动辅助线程而调用的代码。代码做的第一件事是设置CFMessagePortRef
不透明类型来监听来自工作线程的消息。辅助线程需要端口的名称才能进行连接,以便将字符串值交付到辅助线程的入口点函数。端口名称在当前用户上下文中通常应该是唯一的;否则,您可能会遇到冲突。
列出3-17将Core Foundation消息端口附加到新线程
#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所示。此函数提取辅助线程的端口名称,并为未来的通信创建一个管道。
#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显示了工人线程的入口点函数。该函数提取主线程的端口名称,并使用它创建返回主线程的远程连接。然后,该函数为自己创建一个本地端口,在线程的运行循环上安装该端口,并向包含本地端口名称的主线程发送签入消息。
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函数处理。该函数的实现取决于线程所做的工作类型,此处不显示。