Run Loops
运行循环是与线程相关联的基础架构的一部分。runloop是一个事件处理循环,你可以使用它来处理你安排的工作和协调处理传入的事件。runloop的目的就是当有任务的时候一直保持线程处理繁忙状态,当没有任务的时候让线程处于休眠状态。用来节约系统资源。
Cocoa和Core Foundation都提供runloop,以帮助您配置和管理线程的运行循环。您的应用程序不需要明确创建这些对象;每个线程,包括应用程序的主线程,都有一个关联的运行循环对象。但是,只有在子线程需要显式运行它们的运行循环。应用程序框架在应用程序启动过程的一部分,自动设置和运行主线程上的运行循环。
Run Loop 解析
运行循环从两种不同类型的源接收事件,输入源提供异步事件,通常来自另一个线程或不同应用程序的消息。定时器源提供同步事件,发生在预定时间或重复间隔。这两种类型的源使用应用程序在特定的情况下处理事件。
图3-1显示了运行循环和各种源的概念结构。输入源将异步事件传递给相应的处理程序,并导致runUntilDate:方法在一定时间内(在线程的关联NSRunLoop对象上调用)退出。定时器源将事件传递给其处理程序例程,但不会导致运行循环退出。
图3-1运行循环及其源的结构除了处理输入源之外,运行循环还会生成关于运行循环行为的通知。注册的运行循环观察器(observers)可以接收这些通知,并使用它们对线程执行其他处理。您使用Core Foundation在您的线程上安装run-loop观察器。
以下部分提供有关运行循环的组件及其操作模式的更多信息。他们还描述了在处理事件时在不同时间生成的通知。
Run Loop 模式
NSDefaultRunLoopMode(Cocoa)
kCFRunLoopDefaultMode(Core Foundation)
默认模式是用于大多数操作的模式。大多数情况下,您应该使用此模式启动运行循环并配置输入源。
NSConnectionReplyMode(Cocoa)
Cocoa使用此模式结合NSConnection对象来监视回复。你很少需要自己使用这种模式。
NSModalPanelRunLoopMode(Cocoa)
Cocoa使用此模式来识别用于模态面板的事件。
NSEventTrackingRunLoopMode(Cocoa)
Cocoa使用此模式来限制除了在鼠标拖动和用户界面跟踪循环中的其他类型的传入事件。
NSRunLoopCommonModes(Cocoa)
kCFRunLoopCommonModes(Core Foundation)
这是一组可配置的通用模式。将输入源与此模式相关联还将其与组中的每种模式相关联。对于Cocoa应用程序,默认情况下,此设置包括默认模式和事件跟踪模式。Core Foundation最初只包含默认模式。您可以使用该CFRunLoopAddCommonMode功能将自定义模式添加到集合中。
Input Sources(输入源)
输入源与您的线程异步传递事件。事件的来源取决于输入源的类型,这通常是两个类别之一。基于端口的输入源监视应用程序的Mach端口。自定义输入源监视自定义的事件源。就您的运行循环而言,输入源是基于端口还是自定义都不重要。系统通常实现两种类型的输入源,您可以使用它们。两个来源之间的唯一区别是如何发出信号。基于端口的源由内核自动发出信号,自定义源必须从另一个线程手动发出信号。
创建输入源时,将其分配给运行循环的一个或多个模式。模式会影响在任何给定时刻监控哪些输入源。大多数情况下,您在默认模式下运行运行循环,但也可以指定自定义模式。如果输入源不在当前监视的模式,则其生成的任何事件将保持,直到运行循环以正确的模式运行。
以下部分介绍一些输入源。
基于端口的源
Cocoa和Core Foundation为使用端口相关对象和功能创建基于端口的输入源提供内置支持。例如,在Cocoa中,您根本不必直接创建输入源。您只需创建一个端口对象,并使用将该NSPort端口添加到运行循环的方法。端口对象为您处理所需的输入源的创建和配置。
在Core Foundation中,您必须手动创建端口及其运行循环源。在这两种情况下,您使用的端口类型不透明(相关的功能CFMachPortRef,CFMessagePortRef或CFSocketRef)创建合适的对象。
有关如何设置和配置自定义基于端口的源的示例,请参阅配置基于端口的输入源。
自定义输入源
要创建自定义输入源,必须CFRunLoopSourceRef在Core Foundation中使用与不透明类型相关联的函数。您可以使用多个回调函数配置自定义输入源。Core Foundation在不同点调用这些函数来配置源,处理任何传入的事件,并在从运行循环中删除源时将其拆下。
除了在事件到达时定义自定义源的行为之外,还必须定义事件传递机制。源的这一部分运行在一个单独的线程上,负责向输入源提供其数据,并在该数据准备好进行处理时发出信号。事件传递机制取决于您,但不必过于复杂。
有关如何创建自定义输入源的示例,请参阅定义自定义输入源。有关自定义输入源的参考信息,请参见“CFRunLoopSource参考”。
Cocoa Perform Selector 源
除了基于端口的源之外,Cocoa还定义了一个自定义输入源,允许您在任何线程上执行选择器。像基于端口的源一样,执行选择器请求在目标线程上被序列化,减轻了在一个线程上运行多个方法可能发生的许多同步问题。与基于端口的源不同,执行选择器源在执行其选择器后从运行循环中删除自身。
当在另一个线程上执行选择器时,目标线程必须具有活动的运行循环。对于您创建的线程,这意味着等待直到您的代码显式启动运行循环。因为主线程启动自己的运行循环,所以一旦应用程序调用applicationDidFinishLaunching:应用程序委托的方法,就可以开始在该线程上发出调用。运行循环每次循环处理所有排队的执行选择器调用,而不是在每个循环迭代期间处理一个。
以下是在其他线程上执行选择器:
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:方法取消发送到当前线程的消息。
定时器源
定时器源将来会在预设的时间内向线程同步传送事件。计时器是线程通知自己做某事的一种方式。
虽然它生成基于时间的通知,但定时器不是实时机制。与输入源一样,定时器与运行循环的特定模式相关联。如果定时器未处于运行循环当前正在监视的模式,则在运行运行循环的定时器支持的模式之一之前,它不会触发。类似地,如果在运行循环处于执行处理程序例程的中间时定时器触发,则定时器等待直到下一次通过运行循环来调用其处理程序例程。如果运行循环没有运行,则定时器永远不会触发。
您可以配置计时器一次或重复生成事件。重复定时器根据预定的开始时间自动重新调度,而不是实际的开始时间。例如,如果定时器计划在特定时间和之后的每5秒开始,则即使实际的开始时间延迟,预定的开始时间将始终落在原始的5秒时间间隔上。如果开始时间延迟太多,以至于它错过了一个或多个预定的开始时间,那么定时器只会在错过的时间段内被触发一次。对于错过的周期,计时器将重新安排下次计划的开始时间。
有关配置定时器源的更多信息,请参阅配置定时器源。有关参考信息,请参阅NSTimer类参考或CFRunLoopTimer参考。
Run Loop Observers(观察者)
与发生适当的异步或同步事件时发生的源相反,在运行循环本身的执行期间,运行循环观察者在特殊位置触发。您可以使用运行循环观察器来准备您的线程来处理给定的事件,或者在进入休眠之前准备线程。您可以在运行循环中将运行循环观察者与以下事件相关联:
运行循环的入口。
当运行循环即将处理定时器时。
当运行循环即将处理输入源时。
当运行循环即将去睡觉时。
当运行循环已经唤醒,但在它已经处理了唤醒它的事件之前。
退出运行循环。
您可以使用Core Foundation将运行循环观察器添加到应用程序。要创建一个运行循环观察器,您将创建一个CFRunLoopObserverRef不透明类型的新实例。该类型跟踪您的自定义回调函数及其感兴趣的活动。
类似于定时器,可以使用一次或多次运行循环观察器。单次观察器在触发后将其从运行循环中移除,而重复的观察器仍然被附加。您指定观察者在创建它时是否运行一次或多次。
有关如何创建运行环观察器的示例,请参阅配置运行循环。有关参考信息,请参阅CFRunLoopObserver参考。
Run Loop 事件执行顺序
每次运行它时,您的线程的运行循环处理等待事件,并为任何附加的观察者生成通知。这样做的顺序是非常具体的,如下所示:
1 通知观察者已经进入了运行循环。
2 通知观察者,即将处理任何已经准备好的时间器源。
3 通知观察员,即将处理任何不基于端口的输入源。
4 处理非基于端口的输入源。
5 如果基于端口的输入源准备就绪并等待触发,则立即处理该事件。转到步骤9。
6 通知观察者线程即将休眠。
7 让线程休眠,直到发生以下事件之一:
基于端口的输入源事件即将到达。
定时器触发
为运行循环设置的超时值过期。
运行循环被明确唤醒。
8 通知观察者线程被唤醒。
9 处理挂起的事件。
如果用户定义的定时器触发,则处理定时器事件并重新启动循环。转到步骤2。
如果输入源触发,则传递事件。
如果运行循环被明确唤醒但尚未超时,请重新启动循环。转到步骤2。
10 通知观察者运行循环已经退出。
因为定时器和输入源的观察者通知在这些事件实际发生之前被传递,所以在通知的时间和实际事件的时间之间可能存在差距。如果这些事件之间的时机是非常重要的,您可以使用睡眠和睡眠唤醒通知来帮助您将实际事件之间的时间相关联。
因为在运行运行循环时会传递定时器和其他周期性事件,所以绕过该循环会中断这些事件的传递。当您通过输入循环并重复地从应用程序请求事件来实现鼠标跟踪例程时,就会发生此行为的典型示例。因为您的代码直接抓取事件,而不是让应用程序正常发送这些事件,否则激活的计时器将无法启动,直到您的鼠标跟踪程序退出并返回到应用程序的控制。
运行循环可以使用运行循环对象显式唤醒。其他事件也可能导致运行循环被唤醒。例如,添加另一个非基于端口的输入源唤醒运行循环,以便可以立即处理输入源,而不是等到发生其他事件。
什么时候使用Run Loop
您唯一需要显式运行运行循环的方法是为应用程序创建子线程。应用程序主线程的运行循环是基础架构的关键。因此,应用程序框架提供了运行主应用程序循环并自动启动该循环的代码。所述run的方法UIApplication在IOS(或NSApplication在OS X)启动应用程序的主循环的正常启动序列的一部分。如果您使用Xcode模板项目来创建应用程序,则不应显式地调用这些例程。
对于子线程,您需要确定是否需要运行循环,如果是,请自行配置并启动它。在所有情况下,您不需要启动线程的运行循环。例如,如果使用线程执行一些长时间运行和预定的任务,那么您可以避免启动运行循环。运行循环适用于您希望与线程进行更多交互的情况。例如,如果您计划执行以下任何操作,则需要启动运行循环:
使用端口或自定义输入源与其他线程进行通信。
在线程上使用计时器。
performSelector在Cocoa应用程序中使用任何...方法。
保持线程执行定期任务。
如果您选择使用运行循环,则配置和设置很简单。与所有线程编程一样,您应该有一个在适当情况下退出子线程的计划。通过让它退出而不是强制它终止,总是更好地结束一个线程。有关如何配置和退出运行循环的信息,请参见“运行循环对象”。
使用 Run Loop 对象
运行循环对象提供了将输入源,计时器和运行循环观察器添加到运行循环然后运行的主接口。每个线程都有一个与之相关联的运行循环对象。在Cocoa中,此对象是NSRunLoop该类的一个实例。在底层应用程序中,它是一个指向CFRunLoopRef不透明类型的指针。
获取运行循环对象
要获取当前线程的运行循环,请使用以下之一:
在Cocoa应用程序中,使用NSRunLoop的类方法currentRunLoop来获取一个NSRunLoop对象。
在Core Foundation中使用 CFRunLoopGetCurrent() 来获取当前Run Loop。
虽然它们不是免费的桥接类型,但是在需要时可以CFRunLoopRef从NSRunLoop对象获取不透明的类型。本NSRunLoop类定义了一个getCFRunLoop返回的方法CFRunLoopRef类型,你可以传递给Core Foundation的例程。因为两个对象引用相同的运行循环,所以可以根据需要将NSRunLoop对象和CFRunLoopRef不透明类型的调用混合起来。
配置Run Loop
在子线程上运行运行循环之前,必须至少添加一个输入源或定时器。如果运行循环没有任何来源进行监视,则当您尝试运行它时会立即退出。有关如何向运行循环添加源的示例,请参阅配置运行循环源。
除了安装源之外,您还可以安装运行循环观察者并使用它们来检测运行循环的不同执行阶段。要安装运行循环观察者,您将创建一个CFRunLoopObserverRef不透明类型,并使用该CFRunLoopAddObserver函数将其添加到运行循环中。必须使用Core Foundation创建运行循环观察者,即使对于Cocoa应用程序也是如此。
下面显示了将运行循环观察器附加到其运行循环的线程的主例程。该示例的目的是向您展示如何创建运行循环观察器,因此该代码只需设置一个运行循环观察器来监视所有运行循环活动。在处理定时器请求时,基本处理程序例程(未显示)仅记录运行循环活动。
// 用来监听的函数
static void myRunLoopObserver (CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"进入");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"即将处理Timer事件");
break;
case kCFRunLoopBeforeSources:
NSLog(@"即将处理Source事件");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"即将休眠");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"被唤醒");
break;
case kCFRunLoopExit:
NSLog(@"退出RunLoop");
break;
default:
break;}}
- (void)threadMain
{
// 获取当前runloop
NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
// 创建一个观察者并添加到runloop中
CFRunLoopObserverContext context = {0, (__bridge void *)(self), NULL, NULL, NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities, YES, 0, &myRunLoopObserver, &context);
if (observer){
// 添加观察者
CFRunLoopRef cfLoop = [myRunLoop getCFRunLoop];
CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);}
// 创建timer scheduld已经自动添加到runloop中了
[NSTimer scheduledTimerWithTimeInterval:1 target:self
selector:@selector(doFireTimer:) userInfo:nil repeats:YES];
NSInteger loopCount = 10;
do{
// 这里是主线程的runloop 在主线程中无法退出 ,在子线程中这里会退出
[myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
loopCount--;}
while (loopCount);}
-(void)doFireTimer:(id)timer
{
NSLog(@"doFireTimer");
}
配置长时间线程的运行循环时,最好至少添加一个输入源来接收消息。虽然可以仅使用定时器连接进入运行循环,但一旦定时器触发,通常会失效,这将导致运行循环退出。安装重复的定时器可以使运行循环在更长的时间内运行,但会涉及定时启动定时器来唤醒线程,这实际上是另一种形式的轮询。相比之下,输入源等待事件发生,保持线程睡着,直到它发生。
开启Run Loop
启动运行循环仅对应用程序中的辅助线程是必需的。运行循环必须至少有一个输入源或定时器来监视。如果没有,运行循环将立即退出。
有几种启动运行循环的方法,包括:
run 无条件
runUntilDate: 设定时限
runMode:beforeDate: 在特定模式下
无条件进入运行循环是最简单的选择,但也是最不可取的。无条件地运行您的运行循环将线程放入永久循环,这使您无需对运行循环本身的控制。您可以添加和删除输入源和计时器,但停止运行循环的唯一方法是将其删除。也没有办法在自定义模式下运行运行循环。
相比无条件地运行运行循环,最好用runUntilDate运行循环。当您使用runUntilDate时,运行循环将运行,直到事件到达或分配的时间到期。如果一个事件到达,则将该事件分派到处理程序进行处理,然后运行循环退出。您的代码可以重新启动运行循环来处理下一个事件。如果分配的时间到期,您可以简单地重新启动运行循环,或者使用时间进行所需的工作。
除了runUntilDate之外,您还可以使用特定模式运行运行循环。特定模式和runUntilDate不是互斥的,并且可以在启动运行循环时使用。特定模式限制将事件传递到运行循环的源的类型,并在运行循环模式中更详细地描述。
列表3-2显示了线程主入口例程的骨架版本。该示例的关键部分显示了运行循环的基本结构。实质上,您将输入源和定时器添加到运行循环中,然后重复调用其中一个例程来启动运行循环。每次运行循环例程返回时,您都会检查是否出现可能有可能退出线程的条件。该示例使用Core Foundation运行循环例程,以便它可以检查返回结果并确定运行循环退出的原因。您也可以使用NSRunLoop类的方法以类似的方式运行运行循环,如果您使用Cocoa并且不需要检查返回值
注:官网的例子只是写了在主线程运行一个特定模式的Run Loop,下面我进行了一些修改,在子线程中执行。添加一个定时器,然后将定时器添加到runloop中,runloop运行模式是10秒钟,所以后面打印之后runloop变会退出,定时器就不会执行了。
- (void)skeletonThreadMain
{
// 在子线程中执行
NSLog(@"%@",[NSThread currentThread]);
// 获取当前runloop
CFRunLoopRef runLoop =CFRunLoopGetCurrent();
// 上下文
CFRunLoopTimerContext context = {0,NULL,NULL,NULL,NULL};
// 创建timer
CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault,1,3,0,0,
&myCFTimerCallback,&context);
// 添加到runloop中
CFRunLoopAddTimer(runLoop,timer,kCFRunLoopCommonModes);
BOOL done = NO;
do{
SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);
if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished ||result ==kCFRunLoopRunTimedOut ))
done = YES;
NSLog(@"runloop 已经退出");}
while (!done);
}
// 使用Core Foundation创建和计划定时器
void myCFTimerCallback(CFRunLoopTimerRef timer, void *info)
{
NSLog(@"myCFTimerCallback");
}
2017-09-08 16:49:22.637 runloop应用[75136:41331947]{number = 3, name = (null)}
2017-09-08 16:49:22.637 runloop应用[75136:41331947] myCFTimerCallback
2017-09-08 16:49:25.641 runloop应用[75136:41331947] myCFTimerCallback
2017-09-08 16:49:28.638 runloop应用[75136:41331947] myCFTimerCallback
2017-09-08 16:49:31.637 runloop应用[75136:41331947] myCFTimerCallback
2017-09-08 16:49:32.638 runloop应用[75136:41331947] runloop 已经退出
可以递归运行一个运行循环。换句话说,您可以从输入源或定时器的处理程序例程中调用CFRunLoopRun,CFRunLoopRunInMode或任何NSRunLoop方法来启动运行循环。当这样做时,您可以使用任何要运行嵌套运行循环的模式,包括外部运行循环使用的模式。
退出Run Loop
在处理事件之前,有两种方法使运行循环退出:
配置运行循环以超时值运行。(上面例子中已经写出来了)
告诉运行循环停止。(下面例子中)
使用超时值当然是首选,如果您可以管理它。指定超时值可以使退出之前的运行循环完成所有正常处理,包括传递通知以运行循环观察器。
使用该CFRunLoopStop函数显式停止运行循环会产生类似于超时的结果。运行循环发出任何剩余的运行循环通知,然后退出。不同的是,您可以在无条件启动的运行循环上使用此技术。
尽管删除运行循环的输入源和计时器也可能导致运行循环退出,但这不是停止运行循环的可靠方法。一些系统例程将输入源添加到运行循环以处理所需的事件。因为您的代码可能不知道这些输入源,它将无法删除它们,这将阻止运行循环退出。
下面例子展示CFRunLoopStop的使用
static int i=0;
// 使用Core Foundation创建和计划定时器
void myCFTimerCallback(CFRunLoopTimerRef timer, void *info)
{
NSLog(@"myCFTimerCallback");
if (i==4){
CFRunLoopStop(CFRunLoopGetCurrent());
NSLog(@"runloop 退出");}
i++;
}
- (void)skeletonThreadMainstop
{
// 在子线程中执行
NSLog(@"%@",[NSThread currentThread]);
// 获取当前runloop
CFRunLoopRef runLoop =CFRunLoopGetCurrent();
// 上下文
CFRunLoopTimerContext context = {0,NULL,NULL,NULL,NULL};
// 创建timer
CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault,1,3,0,0,
&myCFTimerCallback,&context);
// 添加到runloop中
CFRunLoopAddTimer(runLoop,timer,kCFRunLoopCommonModes);
CFRunLoopRun();
}
2017-09-08 17:21:21.991 runloop应用[75819:41527674]{number = 4, name = (null)}
2017-09-08 17:21:21.992 runloop应用[75819:41527674] myCFTimerCallback
2017-09-08 17:21:24.992 runloop应用[75819:41527674] myCFTimerCallback
2017-09-08 17:21:27.993 runloop应用[75819:41527674] myCFTimerCallback
2017-09-08 17:21:30.992 runloop应用[75819:41527674] myCFTimerCallback
2017-09-08 17:21:33.992 runloop应用[75819:41527674] myCFTimerCallback
2017-09-08 17:21:33.992 runloop应用[75819:41527674] runloop 退出
上面打印信息可以看出runloop使用stop函数退出了。
线程安全和Run Loop对象
线程安全性取决于您用来操作运行循环的API。Core Foundation中的功能通常是线程安全的,可以从任何线程调用。但是,如果您正在执行改变运行循环配置的操作,那么尽可能从拥有运行循环的线程执行此操作仍然是最佳做法。
CoCoaNSRunLoop类并不像其Core Foundation的一般线程一样安全。如果您正在使用NSRunLoop该类来修改运行循环,那么您只能从拥有该运行循环的同一线程执行此操作。将输入源或计时器添加到属于不同线程的运行循环可能会导致代码崩溃或出现意外的行为。
网友评论