Run Loops
-
Run Loops描述
(官方文档)Run Loops(运行循环)是与线程相关的基础架构的一部分。一个运行循环是指用于安排任务,并协调接收传入事件的事件处理循环。运行循环的目的是在有工作时保持线程活跃,并在没有操作时让线程进入休眠状态。
一般来讲,一个线程(非主线程)一次只能执行一个任务,执行完后线程就会退出。Run Loops 机制就是让线程能随时处理事件而不退出,忙时活跃,闲时休眠。
主线程上的run loop对象是默认自动开启的,其他子线程是默认没有开启run loop 对象的。如何对子线程启动Run Loops 机制?Cocoa和Core Foundation都提供了run loop对象去给线程配置和管理run loop对象。 -
Run Loop与线程的关系
首先,iOS 开发中能遇到两个线程对象: pthread_t 和 NSThread。过去苹果有份文档标明了 NSThread 只是 pthread_t 的封装,但那份文档已经失效了,现在它们也有可能都是直接包装自最底层的 mach thread。苹果并没有提供这两个对象相互转换的接口,但不管怎么样,可以肯定的是 pthread_t 和 NSThread 是一一对应的。比如,你可以通过 pthread_main_thread_np() 或 [NSThread mainThread] 来获取主线程;也可以通过 pthread_self() 或 [NSThread currentThread] 来获取当前线程。CFRunLoop 是基于 pthread 来管理的。
苹果不允许直接创建 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。 这两个函数内部的逻辑大概是下面这样:
/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 访问 loopsDic 时的锁
static CFSpinLock_t loopsLock;
/// 获取一个 pthread 对应的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
OSSpinLockLock(&loopsLock);
if (!loopsDic) {
// 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
loopsDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
}
/// 直接从 Dictionary 里获取。
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
if (!loop) {
/// 取不到时,创建一个
loop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, thread, loop);
/// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
_CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
}
OSSpinLockUnLock(&loopsLock);
return loop;
}
CFRunLoopRef CFRunLoopGetMain() {
return _CFRunLoopGet(pthread_main_thread_np());
}
CFRunLoopRef CFRunLoopGetCurrent() {
return _CFRunLoopGet(pthread_self());
}
从上面的代码可以看出来线程和RunLoop是一一对应的,存在一个全局的Dictionary里,以pthread_t为key 去取相应的RunLoop。相当于懒加载一样,为对应的线程添加RunLoop会先检查当前线程是否已有RunLoop对象,有则直接获取,若没有则创建一个新RunLoop,并注册一个回调,当线程销毁时也销毁对应的RunLoop
-
Run Loop Modes
Run Loop Mode 是一个输入源(Source)和定时器(Timer)的集合对事件其进行监听,以及对RunLoop Observers进行通知。Cocoa 和 Core Foundation定义的Mode
Mode | Name | Description |
---|---|---|
Default | NSDefaultRunLoopMode(Cocoa)kCFRunLoopDefaultMode(Core Foundation) | 默认模式是用于大多数操作的模式。大多数情况下,您应该使用此模式启动运行循环并配置输入源 |
Connection | NSConnectionReplyMode(Cocoa) | Cocoa将此模式与NSConnection对象结合使用以监视回复。您自己应该很少需要使用此模式。 |
Modal | NSModalPanelRunLoopMode(Cocoa) | Cocoa使用此模式来识别用于模态面板的事件。 |
Event tracking | NSEventTrackingRunLoopMode(Cocoa) | Cocoa使用此模式限制拖动循环和其他种类的用户界面跟踪循环期间传入事件。(如滚动ScrollView时) |
Common modes | NSRunLoopCommonModes(Cocoa)kCFRunLoopCommonModes(Core Foundation) | 这是一组可配置的常用模式。将输入源与此模式相关联也会将其与组中的每个模式相关联。对于Cocoa应用程序,此集合默认包括Default,Modal和Event tracking模式。Core Foundation最初只包含Default模式。您可以使用该CFRunLoopAddCommonMode功能将自定义模式添加到集合中。 |
-
Run Loop 事件执行序列
- 通知监听者即将进入run loop
- 通知监听者即将销毁timer
- 通知监听者即将销毁Source0
- 销毁Source0
- 如果存在Source1 跳到第9步
- 通知监听者线程即将进入休眠状态
- 进入休眠状态等待唤醒(Source、timer销毁、run loop timeout、run loop 被唤醒)
- 通知监听者线程被唤醒
- 处理待处理事件:
- 如果触发了用户定义的计时器,则处理计时器事件并重新启动循环。转到第2步
- 如果被触发,则传递该事件
- 如果运行循环被明确唤醒但尚未超时,请重新启动循环。转到第2步。
- 通知监听者即将退出run loop
下面内部实现代码(经整理):
/// 用DefaultMode启动
void CFRunLoopRun(void) {
CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}
/// 用指定的Mode启动,允许设置RunLoop超时时间
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
/// RunLoop的实现
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
/// 首先根据modeName找到对应mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
/// 如果mode里没有source/timer/observer, 直接返回。
if (__CFRunLoopModeIsEmpty(currentMode)) return;
/// 1. 通知 Observers: RunLoop 即将进入 loop。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
/// 内部函数,进入loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
Boolean sourceHandledThisLoop = NO;
int retVal = 0;
do {
/// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 4. RunLoop 触发 Source0 (非port) 回调。
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
if (hasMsg) goto handle_msg;
}
/// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}
/// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
/// • 一个基于 port 的Source 的事件。
/// • 一个 Timer 到时间了
/// • RunLoop 自身的超时时间到了
/// • 被其他什么调用者手动唤醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
}
/// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
/// 收到消息,处理消息。
handle_msg:
/// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}
/// 9.2 如果有dispatch到main_queue的block,执行block。
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}
/// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
else {
CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
if (sourceHandledThisLoop) {
mach_msg(reply, MACH_SEND_MSG, reply);
}
}
/// 执行加入到Loop的block
__CFRunLoopDoBlocks(runloop, currentMode);
if (sourceHandledThisLoop && stopAfterHandle) {
/// 进入loop时参数说处理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout) {
/// 超出传入参数标记的超时时间了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(runloop)) {
/// 被外部调用者强制停止了
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
/// source/timer/observer一个都没有了
retVal = kCFRunLoopRunFinished;
}
/// 如果没超时,mode里没空,loop也没被停止,那继续loop。
} while (retVal == 0);
}
/// 10. 通知 Observers: RunLoop 即将退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}
-
什么时候用Run Loops
首先,应用启动时在main 函数中就已经自动为主线程开启了一个Run Loop。
其次,如果你想执行以下操作时,则需要启用Run Loop:
- 使用端口或自定义输入源与其他线程通信。
- 在线程上使用计时器
- 使用
performSelector...
任何方法 - 保持线程执行周期性任务
-
后记
本文记录是结合深入理解RunLoop这篇文章 、官方文档和CFRunLoop源码进行记录的,深入理解RunLoop这篇文章里面还有一些Run Loop使用场景和底层原理分析,非常值得大家学习
网友评论