Runloop(二)Runloop 的使用

run loop object提供了用于将input sourcestimersrun-loop observers添加到你的run loop然后运行它的主接口。每个线程都有一个与之关联的run loop object。在Cocoa中,这个对象是NSRunLoop类的一个实例。在低级应用程序(low-level application)中,它是一个指向CFRunLoopRef不透明类型的指针。

RunLoop 对象

获取 runloop

要获得当前线程的运行循环,可以使用以下方法之一:

  1. Cocoa应用程序中,使用NSRunLoopcurrentRunLoop类方法来检索NSRunLoop对象。
  2. 使用CFRunLoopGetCurrent函数。

NSRunLoop 类定义了一个 getCFRunLoop 方法,它返回一个可以传递给Core Foundation 例程的 CFRunLoopRef 类型。可以根据需要混合调用NSRunLoop 对象和 CFRunLoopRef 不透明类型。

配置 Run Loop

在辅助线程上运行 run loop 之前,必须向其添加至少一个 input sourcetimer。如果 run loop 没有任何要监视的源,那么在尝试运行它时,它会立即退出。

除了安装sources之外,还可以设置run loop observers,并使用它们来检测 run loop 的不同执行阶段。要安装run loop observers,请创建CFRunLoopObserverRef不透明类型,然后使用CFRunLoopAddObserver函数将其添加到run loop中。run loop observers 必须使用 Core Foundation 创建,即使对于 Cocoa 应用程序也是如此。

如下是创建一个 run loop observer 并添加到 run loop的例子:

1
2
3
4
5
void myObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) { }

[NSThread detachNewThreadWithBlock:^{
[self threadMain];
}];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
- (void)threadMain
{
// 由于应用使用 garbage collection,所以不需要 autorelease pool
NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];

// 创建一个 run loop observer 并添加到 run loop
CFRunLoopObserverContext context = {0, self, NULL, NULL, NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities, YES, 0, &myObserverCallBack, &context);

if (observer)
{
CFRunLoopRef cfLoop = [myRunLoop getCFRunLoop];
CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
}

// 创建并安排 timer.
[NSTimer scheduledTimerWithTimeInterval:0.1 target:self
selector:@selector(doFireTimer:) userInfo:nil repeats:YES];

NSInteger loopCount = 10;
do
{
// 运行 runloop 10次,让计时器触发
[myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
loopCount--;
}
while (loopCount);
}

在为long-lived thread(长生命周期线程)配置 run loop 时,最好至少添加一个input source 来接收消息。尽管你可以在只附加一个 timer 的情况下启动run loop,但一旦timer触发,它通常会失效,然后会导致run loop退出。附加一个重复timer可以使run loop保持较长时间运行,但需要定期触发timer来唤醒线程,这实际上是轮询的另一种形式。相比之下,input source会等待事件发生,使线程保持睡眠状态直到事件发生。

启动 Run Loop

只有应用程序中的子线程才需要启动run looprun loop必须至少有一个要监视的input sourcetimer。如果没有附加,则run loop将立即退出。

有以下几种方式来启动run loop

  • 无条件地,(run 函数)
  • 设置时间限制,(runUntilDate: 函数)
  • 以特定的模式,(runMode:beforeDate: 函数)

无条件地启动run loop是最简单的选择,但也是最不可取的。无条件地运行run loop会将线程放入一个永久循环中,这使你几乎无法控制run loop本身。你可以添加和删除input sourcetimer,但是停止运行循环的唯一方法是终止它。也无法在自定义模式下运行run loop

与其无条件地运行run loop,不如使用timeout value(超时值)运行run loop。使用timeout value时,run loop将运行直到事件到达或分配的时间过期为止。如果事件到达,则将该事件分派到处理程序进行处理,然后退出run loop。然后,你的代码可以重新启动run loop以处理下一个事件。如果分配的时间到期了,可以简单地重新启动run loop或使用该时间进行任何必要的内务处理。

除了timeout value外,还可以使用特定mode运行run loop。模式和超时值不是互斥的,可以在启动run loop时同时使用。模式限制向run loop传递事件的源(sources)的类型。

如下例子运行 runloop:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)skeletonThreadMain
{
// 如果不使用 garbage collection,请在此处设置 autorelease pool
BOOL done = NO;

// 添加 sources、timers 到 run loop 中,并进行任何其他设置

do
{
// 启动 run loop,但在处理完每个 source 后返回.
SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);

// 如果 source 显式停止了 run loop,或者没有 sources 或 timers,则继续并exit
if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished))
done = YES;

// 检查此处是否存在任何其他退出条件,并根据需要设置done变量
}
while (!done);

// 在这里清理代码。请务必释放任何分配的 autorelease pools
}

可以递归运行run loop。换句话说,可以从input sourcetimer的处理程序例程中调用CFRunLoopRunCFRunLoopRunInMode或任何NSRunLoop方法来启动run loop。这样做时,可以使用任何要运行嵌套run loop的模式,包括外部run loop使用的模式。

退出 Run Loop

在处理事件之前,有两种方法可以使run loop退出:

  • 配置run loop以使用timeout value运行。
  • 告诉run loop停止。

如果你可以管理timeout value的话,使用timeout value当然是首选的。指定timeout value可以让run loop在退出之前完成其所有正常处理,包括向run loop observers发送通知。

使用 CFRunLoopStop 函数显式地停止run loop会产生类似于超时的结果。run loop发送所有剩余的运行循环通知,然后退出。区别在于,可以在无条件启动的run loop上使用此技术。

虽然删除run loopinput sourcestimers 也可能导致 run loop 退出,但这不是停止 run loop 的可靠方法。一些系统例程将 input sources 添加到 run loop 以处理所需的事件。因为你的代码可能不知道这些input sources,所以它将无法删除它们,这将无法使 run loop 退出。

线程安全和运行循环对象

线程安全性取决于你用于操作run loop的 API。Core Foundation中的函数通常是线程安全的,可以从任何线程调用。但是,如果执行的操作更改了 run loop 的配置,则仍然最好从拥有 run loop 的线程进行更改。

CocoaNSRunLoop类并不像它的Core Foundation对应的类那样本质上是线程安全的。如果你使用NSRunLoop类来修改你的run loop,则只能从拥有那个run loop的同一个线程中进行修改。将 input sourcestimers 添加到属于不同线程的run loop中可能会导致代码崩溃或异常行为。

配置 Run Loop Sources

以下各节展示了如何在 CocoaCore Foundation 中设置不同类型的 input sources 的示例。

定义 Custom Input Source

创建Custom Input Source(自定义输入源)需要定义以下内容:

  • 希望 input source 处理的信息。
  • 一个调度程序例程(scheduler routine),让感兴趣的 clients 知道如何联系你的 input source
  • 用于执行任何 clients 发送的请求的handler routine(处理程序例程)。
  • 取消例程(cancellation routine)使input source无效。

因为你创建了一个 Custom Input Source 来处理自定义信息,所以实际的配置设计得很灵活。调度程序(scheduler)、处理程序(handler)和取消例程(cancellation routines)是 custom input source 几乎总是需要的key routines(关键例程)。但是,input source的其余大部分行为发生在这些处理程序例程之外。例如,由你定义将数据传递到input source和将input source的存在与其他线程通信的机制。

如下图显示了 Custom Input Source 的配置示例。在此示例中,应用程序的主线程维护对input source、该input source的自定义命令缓冲区以及安装该input sourcerun loop的引用。

当主线程有需要要移交给worker thread的任务时,它会向command buffer(命令缓冲区)发布命令,以及worker thread启动任务所需的任何信息。(由于主线程和worker threadinput source都可以访问command buffer,因此必须同步访问。)发布命令后,主线程会向input source发出信号,并唤醒worker threadrun loop。收到唤醒命令后,run loop将调用input sourcehandler,该handler将处理在command buffer中找到的命令。

可以从官方文档上找到上图中解释的custom input source的实现,以及你需要实现的关键代码。

配置 Timer Sources

要创建timer source,你要做的就是创建一个timer object并将其安排在run loop上。在Cocoa中,使用NSTimer类创建新的timer objects,在Core Foundation中,使用CFRunLoopTimerRef不透明类型。在内部,NSTimer类只是Core Foundation的扩展,它提供了一些便利功能,例如使用同一方法创建和安排计时器的功能。

Cocoa中,可以使用以下两种方法之一同时创建和安排计时器:

1
2
scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
scheduledTimerWithTimeInterval:invocation:repeats:

上面方法创建timer,并以默认模式(NSDefaultRunLoopMode)将其添加到当前线程的run loop中。如果需要,可以手动安排timer,方法是创建NSTimer对象,然后使用NSRunLoopaddTimer:forMode:方法将其添加到run loop中。这两种技术都做着基本相同的事情,但是可以为你提供对timer配置的不同级别的控制。例如,如果你创建timer并将其手动添加到run loop中,则可以使用默认模式以外的其他模式来执行此操作。

如下使用 NSTimer 创建和调度计时器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];

// 初始延迟为1秒,但此后每0.1秒定期触发一次
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];

// 初始0.2秒延迟后开始触发一次,然后每隔0.2秒触发一次
[NSTimer scheduledTimerWithTimeInterval:0.2
target:self
selector:@selector(myDoFireTimer2:)
userInfo:nil
repeats:YES];

配置 Port-Based Input Source

CocoaCore Foundation 都提供了port-based objects,用于在线程之间或进程之间进行通信。以下各节说明如何使用几种不同类型的port来设置port通信。

配置 NSMachPort

要与 NSMachPort 对象建立本地连接,请创建port object并将其添加到主线程的run loop中。启动辅助线程时,将同一对象传递给线程的entry-point function(入口点函数)。辅助线程可以使用相同的对象将消息发送回主线程。

配置 NSMessagePort

要与 NSMessagePort 对象建立本地连接,不能简单地在线程之间传递端口对象。远程消息端口必须按名称获取。要在 Cocoa 中实现此功能,需要使用特定名称注册你的本地端口,然后将该名称传递给远程线程,以便它可以获取适当的端口对象以进行通信。

学习博客

Threading Programming Guide * Run Loops

NSRunLoop

YYKit大佬 深入理解RunLoop

iOS刨根问底-深入理解RunLoop

iOS 各个线程 Autorelease 对象的内存管理

runloop 孙源优酷视频

文章作者: Czm
文章链接: http://yoursite.com/2020/10/27/Runloop-%E4%BA%8C-Runloop-%E7%9A%84%E4%BD%BF%E7%94%A8/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Czm