版本记录
版本号 | 时间 |
---|---|
V1.0 | 2017.08.23 |
前言
NSRunloop
是OC Foundation
框架中非常重要的一个类,很多时候我们会使用它,但是未必对其有深入的了解,接下来几篇我就会带着大家重新学习一下NSRunloop
这个类,从简单到复杂,从基本到深化,我会一步步的走完。希望对大家有所帮助。感兴趣的可以看我上一篇。
1. NSRunloop简单细说(一)—— 整体了解
2. NSRunloop简单细说(二)—— 获取运行循环及其模式
3. NSRunloop简单细说(三)—— 定时器和端口
4. NSRunloop简单细说(四)—— 开启Runloop
5. NSRunloop简单细说(五)—— 调度和取消消息
6. NSRunloop简单细说(六)—— 几种循环模式详细解析
7. NSRunloop简单细说(七)—— 几个重要的问题(一)
一、Timer Sources - 定时源
定时器来源将在预设的时间内同步向线程传送事件。 定时器是线程通知自己做某事的一种方式。 例如,一旦在来自用户的连续关键笔划之间经过一定量的时间,搜索字段就可以使用定时器来启动自动搜索。 使用这个延迟时间给用户在开始搜索之前有机会输入尽可能多的期望的搜索字符串。
虽然它生成基于时间的通知,但定时器不是实时机制。 与输入源一样,定时器与运行循环的特定模式相关联。 如果定时器未处于运行循环当前正在监视的模式,则在您以其中一个定时器支持的模式运行运行循环之前,它不会触发。 类似地,如果在运行循环处于执行处理程序例程的中间时定时器触发,则定时器等待直到下一次通过运行循环来调用其处理程序例程。 如果运行循环没有运行,则定时器不会被触发。
您可以配置定时器一次或重复生成事件。 重复定时器根据预定的开启时间自动重新调度,而不是实际的开启时间。 例如,如果定时器计划在特定时间和之后的每5秒发射,则即使实际的发射时间延迟,预定的发射时间将始终落在原始的5秒时间间隔上。 如果开启时间延迟太多,以至于它错过了一个或多个预定的开启时间,则定时器在错过的时间段内仅被触发一次。 对于错过的周期,计时器将重新安排下次预定的开启时间。
二、Run Loop Observers - 运行循环观察
适宜的异步或同步事件发生后会触发事件源的开始,与这些源相反,在运行循环本身的执行期间,运行循环观察器在特定的位置触发。 您可以使用运行循环观察器来准备您的线程来处理给定的事件,或者在进入休眠之前准备线程。 您可以在运行循环中将运行循环观察器与以下事件相关联:
- 运行循环的入口。
- 当运行循环即将处理定时器时。
- 当运行循环即将处理输入源时。
- 当运行循环即将去休眠时。
- 当运行循环已经被唤醒,但在它已经处理了唤醒它的事件之前。
- 退出运行循环。
您可以使用Core Foundation将运行循环观察器添加到应用程序。 要创建一个运行循环观察器,您将创建一个CFRunLoopObserverRef不透明类型的新实例。 这种类型跟踪您的自定义回调函数及其感兴趣的活动。
类似于定时器,可以使用一次或多次运行循环观察器。 单次观察器在触发之后将其从运行循环中删除,而重复的观察器则不会被删除,您指定观察者在创建它时是否运行一次或多次。
三、The Run Loop Sequence of Events - 事件的运行循环序列
每次运行它时,线程的运行循环将处理挂起的事件,并为任何附加的观察者生成通知。 这样做的顺序是非常明确的,如下所示:
-
- 通知观察者已输入运行循环。
-
- 通知观察员,任何准备好的定时器即将开启。
-
- 通知观察员,任何不是基于端口的输入源即将启动。
-
- 任何可以启动的非基于端口的输入源启动。
-
- 如果基于端口的输入源准备就绪并等待触发,请立即处理该事件。 转到步骤9。
-
- 通知观察者线程即将睡眠。
-
- 让线程睡觉,直到发生以下事件之一:
- 事件到达基于端口的输入源。
- 一个计时器触发
- 为运行循环设置的超时值过期。
- 运行循环被明确唤醒。
-
- 通知观察者线程刚刚醒来。
-
- 处理待处理的事件。
- 如果用户定义的定时器触发,则处理定时器事件并重新启动循环。 转到步骤2。
- 如果输入源被触发,则传递该事件。
- 如果运行循环被明确唤醒但尚未超时,请重新启动循环。 转到步骤2。
-
- 通知观察者运行循环已经退出。
因为定时器和输入源的观察者通知在这些事件实际发生之前传递,所以通知时间和实际事件的时间之间可能存在差距。 如果这些事件之间的时间是至关重要的,您可以使用睡眠和睡眠唤醒通知来帮助您将实际事件之间的时间相关联。
因为定时器和其他周期性事件在运行运行循环时传递,规避该循环会破坏这些事件的传递。 一个典型的例子就是,当您通过输入循环并重复地从应用程序请求事件来实现鼠标跟踪时,这种行为就会发生。 因为您的代码直接抓取事件,而不是让应用程序正常发送这些事件,所以活动计时器将无法启动,直到您的鼠标跟踪程序退出并返回控制应用程序。
运行循环可以使用运行循环对象显式唤醒。 其他事件也可能导致运行循环被唤醒。 例如,添加另一个非基于端口的输入源唤醒运行循环,以便可以立即处理输入源,而不是等待直到发生其他一些事件。
四、什么时候使用Run Loop?
您唯一需要显式运行运行循环就是在为应用程序创建辅助线程的时候,您的应用程序主线程的运行循环是程序的一个关键部分。 因此,app框架提供了运行主应用程序循环并自动启动该循环的代码。 iOS中的UIApplication(或OS X中的NSApplication)的运行方法作为正常启动顺序的一部分启动应用程序的主循环。 如果您使用Xcode模板项目来创建应用程序,则不应显式地调用这些例程。
对于辅助线程,你也可以理解为子线程。您需要确定是否需要运行循环,如果是,请自行配置并启动它。 在所有情况下,您不需要启动线程的运行循环。 例如,如果使用线程执行一些长时间运行和预定的任务,那么可以避免启动运行循环。 运行循环旨在用于与线程进行更多交互的情况。 例如,如果您计划执行以下操作,则需要启动运行循环:
- 使用端口或自定义输入源与其他线程通信。
- 在线程上使用计时器。
- 在Cocoa应用程序中使用任何performSelector ...方法。
- 保持线程执行定期任务
如果您选择使用运行循环,则配置和设置很简单。 与所有线程编程一样,您应该有一个在适当情况下退出辅助线程的计划,让它明确的退出要比强制它退出要好的多。
五、使用 Run Loop对象
运行循环对象提供了将输入源,计时器和运行循环观察器添加到运行循环然后运行的主接口。 每个线程都有一个与之相关联的运行循环对象。 在Cocoa中,此对象是NSRunLoop
类的一个实例。 在低级应用程序中,它是指向CFRunLoopRef
不透明类型的指针。
1. Getting a Run Loop Object - 获取 Run Loop对象
要获取当前线程的运行循环,请使用以下之一:
- 在Cocoa应用程序中,使用
NSRunLoop
的currentRunLoop
类方法来检索NSRunLoop对象。 - 使用
CFRunLoopGetCurrent
函数
虽然它们不是自由的桥接类型,但是在需要时可以从NSRunLoop对象获取CFRunLoopRef不透明类型。 NSRunLoop类定义了一个getCFRunLoop方法,该方法返回可以传递给Core Foundation例程的CFRunLoopRef类型。 因为两个对象引用相同的运行循环,所以可以根据需要将NSRunLoop对象和CFRunLoopRef opaque类型的调用混合。
2. Configuring the Run Loop - Run Loop的配置
在子线程上运行运行循环之前,必须至少添加一个输入源或计时器。 如果运行循环没有任何来源进行监视,则当您尝试运行它时立即退出。
除了安装源之外,您还可以安装运行循环观察器并使用它们来检测运行循环的不同执行阶段。 要安装运行循环观察器,请创建CFRunLoopObserverRef opaque类型,并使用CFRunLoopAddObserver函数将其添加到运行循环中。 必须使用Core Foundation创建运行循环观察器,即使对于Cocoa应用程序也是如此。
下面代码显示了将运行循环观察器连接到其运行循环的线程的主例程。 该示例的目的是向您展示如何创建运行循环观察器,因此该代码只需设置一个运行循环观察器来监视所有运行循环活动。 在处理定时器请求时,基本处理程序例程(未显示)只记录运行循环活动。
//创建运行循环观察器
- (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);
}
配置长时间存在的线程的运行循环时,最好至少添加一个输入源来接收消息。 虽然可以只连接一个计时器来进入运行循环,但一旦定时器触发,通常会使其无效,这将导致运行循环退出。 安装重复的计时器可以使运行循环在较长的时间内运行,但是会涉及到定时唤醒定时器来唤醒你的线程,这实际上是另一种形式的轮询。 相比之下,输入源等待事件发生,保持线程睡着,直到它发生。
3. Starting the Run Loop - 开启运行循环
启动运行循环仅对应用程序中的辅助线程是必需的。 运行循环必须至少有一个输入源或定时器来监视。 如果没有附加,运行循环将立即退出。
启动运行循环有几种方法,包括:
- 无条件
- 设定时限
- 在特定模式下
无条件进入你的运行循环是最简单的选择,但它也是不太可取的。 无条件地运行您的运行循环将线程放入永久循环,这使您对运行循环本身的控制能力变的很弱。 您可以添加和删除输入源和计时器,但停止运行循环的唯一方法是将其删除。 也没有办法在自定义模式下运行运行循环。
相对无条件地运行运行循环,最好用超时值运行运行循环。 当您使用超时值时,运行循环将运行,直到事件到达或分配的时间到期。 如果一个事件到达,则将该事件分派到处理程序进行处理,然后运行循环退出。 您的代码可以重新启动运行循环来处理下一个事件。 如果分配的时间到期,您可以简单地重新启动运行循环,或者使用时间来进行所需的内务管理。
除了超时值之外,您还可以使用特定模式运行运行循环。 模式和超时值不是互斥的,并且可以在启动运行循环时使用。 模式限制将事件传递到运行循环的源的类型。
下面代码显示了线程主入口例程。 该示例的关键部分显示了运行循环的基本结构。 实质上,您将输入源和计时器添加到运行循环中,然后重复调用其中一个例程来启动运行循环。 每次运行循环例程返回时,您都会检查是否出现任何可能需要退出线程的条件。 该示例使用Core Foundation
运行循环例程,以便它可以检查返回结果并确定运行循环退出的原因。 您也可以使用NSRunLoop类的方法以类似方式运行运行循环,如果您使用Cocoa并且不需要检查返回值。
//开启运行循环
- (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方法从输入源或计时器的处理程序中启动运行循环。 当这样做时,您可以使用任何要运行嵌套运行循环的模式,包括外部运行循环使用的模式。
4. Exiting the Run Loop - 退出运行循环
在处理事件之前,有两种方法使运行循环退出:
- 配置运行循环以超时值运行。
- 告诉运行循环停止。
使用超时值当然是首选,如果您可以管理它。 指定超时值可让运行循环在退出之前完成其所有正常处理,包括传递运行循环观察器的通知。
使用CFRunLoopStop
函数显式停止运行循环会产生类似于超时的结果。 运行循环发出任何剩余的运行循环通知,然后退出。 不同之处在于,您可以在无条件启动的运行循环中使用此技术。
虽然删除运行循环的输入源和定时器也可能导致运行循环退出,但这不是停止运行循环的可靠方法。 一些系统例程将输入源添加到运行循环以处理所需的事件。 因为你的代码可能不知道这些输入源,它将无法删除它们,这将阻止运行循环退出。
5. Thread Safety and Run Loop Objects - 线程安全和 Run Loop对象
线程安全性取决于您用来操作运行循环的API。 Core Foundation中的功能通常是线程安全的,可以从任何线程调用。 但是,如果您正在执行更改运行循环的配置的操作,那么尽可能从拥有运行循环的线程执行此操作仍然是最佳做法。
Cocoa NSRunLoop
类并不像Core Foundation对象那样固有线程安全。 如果您正在使用NSRunLoop类来修改运行循环,那么只能从拥有该运行循环的同一线程执行此操作。 将输入源或计时器添加到属于不同线程的运行循环可能会导致您的代码崩溃或出现意想不到的方式行为。
后记
未完,待续~~~