知行

NSRunLoop

NSRunLoop类为管理输入源的对象声明程序接口。一个NSRunLoop对象处理输入源:

  • 来自窗口系统的鼠标和键盘事件
  • NSPort对象
  • NSConnection对象
    也处理NSTimer事件。

你的应用程序不能创建也不能明确地管理NSRunLoop对象。每个NSThread对象,包括应用程序主线程,如果需要会自动创建一个NSRunLoop对象。如果你需要获取当前线程的运行循环,可用使用类方法currentRunLoop

注意来自NSRunLoop的观点,NSTimer对象不是“输入”--它们是一个特殊的类型,并且当它们开始不会引起运行循环返回。



RunLoopManagement

Run loops是线程相关基础构造的一部分。一个Run loop是一个安排工作和协调到来事件的事件处理循环。它的目的是有事则忙,无事则休眠。

Run loop管理不是完全自动的。你仍然必须为了在合适的时间开始Run loop和响应到来的事件而设计你的线程代码。CocoaCore Foundation提供了run loop对象来帮助你配置和管理你的线程的run loop。你的应用程序不需要显式创建这些对象;每个线程,包括主线程,拥有一个关联的run loop对象。然而,只有次级的线程需要显式运行它们的run loop对象。应用程序在程序启动时自动在主线程设置和运行主线程的run loop对象。

剖析

Run loop就像它的名字。它是一个循环,你的线程进入并且使用它响应到来的事件进行事件处理。你的代码提供控制语句来实现实际运行循环的一部分--换而言之,你的代码提供whilefor循环以驱动运行循环。在你的循环中,你使用一个运行循环对象来运行事件处理(接收事件和调用安装的处理器)。

Run loop接收两个不同类型的来源的事件。输入源分发异步事件,通常是来自另一个线程或另一个应用程序的信息。时间源分发同步事件,发生在一个安排好的时间或重复的间隔。这两种类型的源使用特殊的程序处理器例行的处理那些到来的事件。

下图显示一个运行循环概念上的结构和不同的源。输入源分发异步事件到相应的处理器并且引发runUntilDate:方法(在线程相关的NSRunLoop对象上调用)来退出。时间源分发事件到处理器,但并不引起运行循环退出。

运行循环结构

在处理输入事件之余,运行循环也发出关于运行循环行为的通知。注册run-loop observers可以接收这些通知并且使用它们在线程上进行额外的处理。你可以使用Core Foundation在你的线程上安装运行循环观察者。

下面提供更多关于运行循环组件和那些在其之上操作的模式的信息。也描述在处理时间过程中在不同的时间产生的通知。

Run Loop Modes

一个运行循环是一个输入源、被监控的计时器和等着被通知运行循环观察者们的集合。每次你运行你的运行循环,你指定(显式或隐式)一个详细的“模式”,在这个模式下运行。在运行循环的运行中,只有跟模式相关的源被监控和允许分发它们的事件。(类似,只有模式相关的观察者才能被通知运行循环的进度。)其它模式相关联的源保留任何新的事件知道在合适的模式透过这个循环。

在你的代码中,你通过名字标识模式。CocoaCore Foundation定义一个默认的模式和几个通常地模式。你可以简单指定一个自定义字符串作为模式名字。尽管自定义模式的名字是随意的,但是模式的内容不是随意的。你必须确保加入一个或多个输入源、计时器或runloop观察者到任何你创建的模式。

你使用模式过滤掉不是想要的源的事件。大多数时间,你想在系统默认模式下运行你的运行循环。

输入源

输入源异步传递事件到线程。事件的源依赖于输入源的类型(两种类型之一)。基于端口的输入源监测你的应用程序的Mach端口。自定义输入源监测自定义事件的源。就run-loop关心的而言,它不应该在意输入源是否是基于端口的或自定义的。

基于端口的源

CocoaCore Foundation为使用端口相关的对象和函数来创建基于端口的源提供内在支持。例如,在Cocoa你不需要直接创建一个输入源。你简单创建一个端口对象并且使用NSPort的方法添加这个端口到运行循环。这个端口对象为你按需创建和配置输入源。

Core Foundataion,你必须手动创建端口和它的运行循环源。在这两种情况下,你使用端口不透明类型(CFMachPortRef, CFMessagePortRef, CFSocketRef)相关的函数创建合适的对象。

自定义输入源

为了创建一个自定义输入源,你必须使用Core Foundation中不透明类型CFRunLoopSourceRef有关的函数。你使用几个回调函数配置自定义输入源。Core Foundation在不同的点调用这些函数来配置源,处理到来的事件和当这个源从运行循环移除时撤下这个源。

另外,为了定一个当事件到达时自定义源的行为,你必须定义事件传递机制。源的这部分在单独的线程上运行并且负责给输入源提供它的数据和打信号。事件传递机制取决于你,但是不需要太复杂。

Perform Selector 源

计时器源

计时器源在预设的时间同步传递事件到你的线程。计时器是一个线程通知自己做一些事情的方式。例如,一个搜索框可以使用计时器在用户输入的一个间隔加入一个自动搜索。这种使用给用户在开始搜索之前一个输入想要的字符串的机会。

尽管它产生基于时间的通知,但是计时器不是实际时间机制。像输入源,计时器关联到指定的模式。如果计时器不是在当前被监测的模式,它不会触发,知道你的运行寻找运行在计时器支持的模式。同样的,如果运行循环是在处理日常中,计时器等到下次调用。如果运行循环不在运行,计时器也就不在触发。

运行循环观察者

观察事件:

  • 进入运行循环
  • 当运行循环将要处理计时器
  • 当运行循环将要处理输入源
  • 当运行循环将要进入休眠
  • 当运行循环被唤醒,但在处理唤醒事件之前
  • 运行循环的退出

你可以使用Core Foundation添加运行循环观察者到程序。为了创建一个运行循环观察者,你创建不透明CFRunLoopObserverRef的实例。这个类型保持你的自定义回调函数和它感兴趣的哪个活动区。

类似于计时器,运行循环观察者可以使用一次或重复使用。

运行循环事件序列

  1. 通知观察者已经进入运行循环
  2. 通知观察者将要处理准备好的计时器
  3. 通知观察着将要处理非基于端口的输入源
  4. 处理非基于端口的输入源
  5. 如果基于端口的输入源准备好了并且等候处理,立即处理这个事件。跳到第9步。
  6. 通知观察者线程将要休眠
  7. 线程休眠,等待唤醒
    • 基于端口的输入源来事件了
    • 计时器触发
    • 对运行循环有效期设置超时数
    • 手动唤醒
  8. 通知观察者线程刚被唤醒
  9. 处理未解决的事件
    • 如果用户定义的计时器触发,处理计时器事件并且重启循环。跳到第2步
    • 如果输入源触发,传递事件
    • 如果运行循环被手动唤醒而且没有超时,重启循环。跳到第2步
  10. 通知观察者运行循环已退出

何时使用运行循环

唯一需要手动运行run loop的时候是你为你的程序创建次级的线程。你程序主线程的run loop是基础构造重要的一部分。所以,app framework为运行主run loop和自动开始运行循环提供代码。

对于次级线程,你需要决定是否有必要一个run loop,如果是,自己配置和开启它。就一般情况而论,你不需要开始一个run loop。例如,如果你使用线程执行长时间运行和预先决定的任务,你可能避免开启run loop。Run loops意在你想要与线程更多交互的情况:

  • 使用端口源或输入源与其它线程交流
  • 在线程上使用计时器
  • 使用任何performSelectro...方法
  • 使线程保持周期性任务

如果你选择使用一个run loop,配置和设置是简单的。

使用运行循环对象

一个Run loop对象为添加输入源、计时器及观察者到你的run loop并接着运行它提供入口。每个线程有一个单一的run loop。

获取一个运行循环对象

  • [NSRunLoop currentLoop]
  • CFRunLoopGetCurrent()

配置运行循环

在你的次级线程上运行运行循环之前,你给它至少添加一个输入源或计时器。如果它没有任何需要监测的源,当你运行时,它会立即退出。

除了安装源,也可以安装运行循环观察者,使用它们发现运行循环的不同阶段。你需要使用不透明类型CFRunLoopObserverRef和方法CFRunLoopAddObserver来添加观察者。

下面的例子展示附加观察者的惯例。这个例子的目的是向你展示如何创建一个观察者,所有这个代码简单设置观察者监测run loop的所有活动。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
void myRunLoopObserver(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"进入Run Loop");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"将要处理计时器");
break;
case kCFRunLoopBeforeSources:
NSLog(@"将要处理源");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"将要休眠");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"休眠结束,准备处理");
break;
case kCFRunLoopExit:
NSLog(@"退出Run Loop");
break;
default:
break;
}
}

- (void)threadMain {
NSRunLoop *myRunLoop = [NSRunLoop currentRunLoop];
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);
}

// 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);
}

- (void)doFireTimer:(id)timer {
NSLog(@"do fire timer");
}

为一个长时间存在的线程配置run loop,最好至少给它添加一个输入源来接收信息。尽管你可以通过附近计时器到run loop来进入run loop,但是计时器一旦用过,它就不可用了,这会引起run loop退出。附近重复性的计时器可以使run loop运行很长的时间,但是会周期性唤醒你的线程,这是轮询的另一种形式。相对的,一个输入源等候事件发生,保持你的线程休眠直到事件发生。

启动运行循环

一个run loop必须至少有一个输入源或计时器,否则运行循环会立即退出。
有几种方式开始运行循环:

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

无条件运行run loop是最简单的选择,但是它最少令人满意的。无条件运行run loop把你的线程放入一个永久的循环,给你非常少的控制它自己。你可以添加或移除源和计时器,但是停止它的唯一方式就是kill它。也没有办法运行它在一个自定义模式。

更好的运行run loop带有超时限制,而不是无条件运行。当你使用一个超时时间,run loop运行直到一个事件到达或者时间过期。如果事件到达,事件被传递去处理接着run loop退出。你的代码可以接着重启run loop去处理下个事件。如果时间到了,你可以重启run loop或者使用这个时间进行管理处理。

除了超时值,你也可以使用一个详细的模式运行run loop。模式和超时不是互斥的,可以同时使用。

下面的例子演示一个线程主要入口惯例的脉络。这个例子的主要部分是展示一个run loop的基本结构。大体上,你添加输入源和计时器到run loop,接着重复调用惯例中的一个开启run loop。每次run loop惯例返回,你检查是否有满足某种条件来保证线程退出。如果不需要检测返回值,可以使用NSRunLoop的方法运行run loop。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (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.
}


可以递归运行run loop。换而言之,你可以在输入源或计时器的惯例处理中调用CFRunLoopCFRunLoopInModeNSRunLoop中任务开启run loop的方法。

退出运行循环

  • 设置超时时间
  • 告诉run loop停止

首选是设置超时时间,如果你能控制它。特别指出,超时时间使run loop在退出之前完成它的所有正常处理,包括传递通知到它的观察者。
使用CFRunLoopStop手动停止run loop产生的结果和超时一样。Run loop发出任何保留的run-loop通知接着退出。不同点在于这个技术是无条件开始的。
尽管移除run loop的输入源和计时器也可能引起run loop退出,但是这不是一个停止run loop的可靠方式。一些系统惯例添加输入源到run loop来处理需要的事件。因为你可能意识不到这些输入源,就不能移除它们,也就不能停止run loop。

线程安全和运行循环对象

Core Foundation中run loop api是线程安全的,而NSRunLoop不是。

配置运行循环源

下面展示设置不同的输入源。

定义一个自定义输入源

涉及到下面:

  • 你想要你的输入源处理的信息
  • 一个调度程序使感兴趣的客户端知道怎样联系你的输入源
  • 一个处理来自任何客户端的处理程序
  • 可以使你的输入源无效的取消程序

下图显示一个自定义输入源的配置例子。这个例子主线程保持对输入源、对输入源的命令缓冲、和安装到的run loop的引用。当主线程有一任务要在工作线程上处理,它发出一个带有任何工作线程需要的任务信息的命令到命令缓冲区。(获取命令需要同步。)一旦命令发出,主线程发信号给输入源并唤醒工作线程的run loop。

操作自定义输入源

定义输入源

定义一个输入源要求使用Core Foundation程序配置你的run loop,并且附加它到run loop。尽管处理程序是基于C的函数,但是不妨碍被封装。

下例中RunLoopSource管理命令缓冲,并且使用该缓冲从其它线程接收消息。这个例子也展示了定义RunLoopContext对象,它是一个容器,用来传递RunLoopSource对象和主线程run loop的引用。

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
30
31
32
33
34
35
@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代码管理输入源的自定义数据,但是附加输入源到run loop要求基于C到回调函数。第一个函数当你实际附加输入源到run loop时调用,下例展示。

1
2
3
4
5
6
7
8
9
void RunLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFStringRef mode)
{
RunLoopSource* obj = (__bridge RunLoopSource*)info;
AppDelegate* del = [AppDelegate sharedAppDelegate];
RunLoopContext* theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl];

[del performSelectorOnMainThread:@selector(registerSource:)
withObject:theContext waitUntilDone:NO];
}

1
2
3
4
5
void RunLoopSourcePerformRoutine (void *info)
{
RunLoopSource* obj = (__bridge RunLoopSource*)info;
[obj sourceFired];
}

1
2
3
4
5
6
7
8
9
void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode)
{
RunLoopSource* obj = (__bridge RunLoopSource*)info;
AppDelegate* del = [AppDelegate sharedAppDelegate];
RunLoopContext* theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl];

[del performSelectorOnMainThread:@selector(removeSource:)
withObject:theContext waitUntilDone:YES];
}

安装输入源到运行循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (id)init
{
CFRunLoopSourceContext context = {0, (__bridge void *)(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);
}

协调输入源的客户端

输入源发信号

配置时间源