RunLoop是线程相关的基础设计。
简介
RunLoop是一个事件处理的循环结构,它可以让线程在无须处理事件时进入休眠状态。RunLoop不是完全自动的,需要在程序中显示设计并调用。在Cocoa中,主线程自动集成了RunLoop,而新建的线程没有。
原理
Structure
RunLoop接收两类源的事件,第一类是Input Sources,负责分发异步事件,用于接收从其他线程或者进程发送过来的消息,第二类是Timer Sources,负责分发同步事件,用于处理计时器事件。
下面是RunLoop的结构图。
可以注册run-loop observers,用于接收RunLoop的行为通知。
Mode
RunLoop Mode是由Input Sources和Timers组成的集合。当设置RunLoop的Mode后,只有与该Mode复合的sources才被允许分发事件。
预先定义好的RunLoop Mode。
Mode | Name | Description |
---|---|---|
Default | NSDefaultRunLoopMode(Cocoa) kCFRunLoopDefaultMode(CoreFoundation) | 默认模式 |
Connection | NSConnectionReplyMode(Cocoa) | 处理NSConnection相关的事件 |
Modal | NSModalPanelRunLoopMode(Cocoa) | 处理Modal Panels相关的事件 |
Event tracking | NSEventTrackingRunLoopMode(Cocoa) | 处理鼠标拖动等界面相关事件 |
Common modes | NSDefaultRunLoopMode(Cocoa) kCFRunLoopDefaultMode(CoreFoundation) | 一组模式组合,Cocoa包含default、modal、event tracking,CoreFoundation只包含default |
对于特定的需求,可以自定义Mode,例如用于过滤一些不需要的事件,或者在对时间有严格要求的操作中,忽略一些低优先级的事件。
自定义Mode方法:
CFRunLoopAddCommonMode
Source
Input Sources
Input Sources,负责分发异步事件.事件的来源取决于source的类型,分为两种:Port-based和Custom。
*Port-Based Sources
由内核自动发出信号,在iOS中不被允许创建。
*Custom Sources
由其他线程发出信号,可以通过CFRunLoopSourceRef方法创建,并通过回调方法进行处理。
下图是一个例子。当主线程有一个任务需要交给Worker线程去处理时,它发送一个命令到command buffer中,完成后,主线程给Input Source发送信号,并唤醒Worker线程的RunLoop。一旦接收到唤醒命令,Worker线程的RunLoop开始处理command buffer中的命令。
*Cocoa Perform Selector Sources
Cocoa定义了一些Sources可以在任何线程上调用的selector.这些sources在执行完selector后,将自动从runLoop中移除。
注意,只有目标线程定义了runLoop,selector sources才会被处理。
Methods | |
---|---|
performSelectorOnMainThread:withObject:waitUntilDone: performSelectorOnMainThread:withObject:waitUntilDone:modes: | |
performSelector:onThread:withObject:waitUntilDone: performSelector:onThread:withObject:waitUntilDone:modes: | |
performSelector:withObject:afterDelay: performSelector:withObject:afterDelay:inModes: | |
cancelPreviousPerformRequestsWithTarget: cancelPreviousPerformRequestsWithTarget:selector:object: |
Timer Sources
Timer Sources,负责分发同步事件. 注意Timer fire并不一定会立刻执行,只有runLoop处理该source时,才会开始执行。
Run Loop Observers
Observers将会监听以下几种RunLoop状态。
- 启动
- 准备处理timer source
- 准备处理Input source
- 准备休眠
- 唤醒,但还未处理事件
- 退出
通过CFRunLoopObserverRef创建Observer实例,可以设置是否重复监听。
事件处理的顺序
Run Loop处理事件的顺序如下:
- (1)通知observers RunLoop启动
- (2)通知observers timers准备启动
- (3)通知observers input sources(非port-based)准备启动
- (4)启动input sources(非port-based)
- (5)如果有input sources(port-based)已准备好,立刻处理该事件,并跳到(9).
- (6)通知observers 线程将休眠
(7)将线程休眠,直到以下几种事件发生:
1. 一个事件到达input sources(port-based) 2. 一个timer启动 3. runLoop超时 4. runLoop被显式唤醒
(8)通知Observers 线程被唤醒
(9)处理等待的事件:
1. 如果一个用户定义的timer启动了,处理该事件,重启runLoop,跳到(2) 2. 如果一个input source启动了,分发该事件 3. 如果runLoop被显式唤醒,并还未超时,重启runLoop,跳到(2)
(10)通知observers RunLoop退出
注意,Observers收到通知和事件真正执行之间存在着时间差,可以通过线程的休眠和唤醒通知来计算该时间差。
一个Run Loop可以通过Run Loop对象显式唤醒,也可以通过事件进行唤醒。
使用
只有手动创建线程时,才可能需要使用RunLoop,需要根据具体使用场景进行分析。对于长时间运行且预先定义好的任务,避免使用RunLoop。
获取RunLoop Object
每个线程有一个单独的RunLoop Object。在Cocoa中,是NSRunLoop,在底层,是CFRunLoopRef的指针。
下面是RunLoop Object的例子。
- (void)threadMain
{
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);
}
启动RunLoop Object
RunLoop启动之前,最好设置超时时间以及模式,不然RunLoop一旦启动,将处于无条件状态,终止的唯一方法是kill。
启动RunLoop Object例子如下:
- (void)skeletonThreadMain
{
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.
}
退出RunLoop Object
退出RunLoop Object有三种方法:
- 设置超时值:RunLoop会完成正常流程,包括发送通知给observers
- 调用CFRunLoopStop方法:RunLoop会发送仍未发送的通知,并退出
- 删除RunLoop所有的sources:不太可能的方法,有些系统会自动添加一些source到RunLoop中,而这些source不是由程序创建的,无法删除,导致RunLoop无法退出。
RunLoop Object的线程安全性
Core Foundation中的方法是线程安全的,可以跨线程调用。而NSRunLoop是非线程安全的,只能在单一线程中被调用,否则将crash。