前几天和同事讨论问题,讨论到 RunLoop 时,发现这个基础概念已经快忘光了,这两天趁着需求完结的空档,整理重温一下。下面的源码来源于 Apple Source Browser CF-1153.18。
RunLoop
RunLoop 从字面直译运行循环,摘录苹果官方的介绍
Run loops are part of the fundamental infrastructure associated with threads. A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.
简单总结就是,RunLoop 可以让你的线程在有任务时持续工作,在空闲时可以进入休眠,并能在有任务出现时及时被唤醒。
如果没有 RunLoop 线程在执行完自己的任务之后就会退出,如果希望在当前任务执行完毕线程不会退出,就需要一个合理的死循环让线程常驻。这样的机制被称为 Event Loop。这种机制并不是 Mac OSX 和 iOS 系统特有的,在绝大多数的系统框架都运用这一优良设计。
在 OSX 和 iOS 系统中,RunLoop 就是一个对象,两个系统的基础框架中提供两个这样的对象:NSRunLoop 和 CFRunLoopRef。
- CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
- NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。
RunLoop 与线程
RunLoop 无法直接创建,但可以通过 CFRunLoopGetMain()
和 CFRunLoopGetCurrent()
来获取主线程或当前线程的 RunLoop 对象。这两个方法的内部都调用了 _CFRunLoopGet0(pthread_t t)
方法,传入的参数分别为 pthread_main_thread_np()
和 pthread_self()
,也和 NSThread 中的 [NSThread mainThread]
和 [NSThread currentThread]
一一对应。继续看下 _CFRunLoopGet0(pthread_t t)
方法的内部,精简一下源码
static CFMutableDictionaryRef __CFRunLoops = NULL;
static CFLock_t loopsLock = CFLockInit;
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) { // 1. 传入的参数如果为 nil,则默认为主线程
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
if (!__CFRunLoops) { // 2. 首次进入时,初始化全局的 Dict __CFRunLoops,并创建一个主线程对应的 run loop
__CFUnlock(&loopsLock);
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
// 3. 从全局的 Dict 中获取当前线程对应的 run loop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
if (!loop) {
// 4. 如果全局 Dict 中不存在则创建新的 run loop 对象,并存储到全局 Dict 中
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
__CFUnlock(&loopsLock);
CFRelease(newLoop);
}
return loop;
}
RunLoop 与线程的关系是一一对应的,使用全局 Hash table __CFRunLoops
来保存 RunLoop 对象和线程的关系,使用线程作为 Key,RunLoop 对象本身为 Value。线程创建的时候并不会主动创建 RunLoop 对象,当我们去获取时才会创建。
RunLoop 的结构
在 CoreFoundation 的源码中,可以找到 RunLoop 的定义 __CFRunLoop
。
struct __CFRunLoop {
...
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode; // 当前执行的 model
CFMutableSetRef _modes; // CFRunLoopModeRef 类型的集合 Set
...
};
从结构体的定义代码中,可以看出一个 RunLoop 对象可以拥有多个 CFRunLoopModeRef
,而当前运行的 Mode 是唯一的。继续看下 CFRunLoopMode
的结构
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
...
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
...
};
在 CFRunLoop.h 文件并没有找到 __CFRunLoopMode
结构体的声明,因此只能通过 CFRunLoopRef
提供的一些 API 对 CFRunLoopMode
中的 sources/observers/timers 进行操作。CoreFoundation 对外暴露了 __CFRunLoopSource
__CFRunLoopObserver
__CFRunLoopTimer
大致可以得出 RunLoop 的结构如下。
一个 RunLoop 对象可以包含多个 RunLoopMode,每个 RunLoopMode 又包含了多个 Sources/Observers/Timers。每次运行一个 RunLoop 都需要指定其中的一个 Mode,当前执行的 Mode 在 __CFRunLoop
结构体的 CFRunLoopModeRef _currentMode;
变量中。
__CFRunLoopSource
在 __CFRunLoopMode
的定义中,有两种 __CFRunLoopSource
的类型,_sources0
和 _sources1
。
- Sources0 包含一个回调,但没有 mach port (mach 端口,后面会提到),这也就限制了 Sources0 无法主动触发事件,需要先使用
CFRunLoopSourceSignal(CFRunLoopSourceRef rls)
将这个 source 标记为 Signaled,再手动调用CFRunLoopWakeUp(CFRunLoopRef rl)
唤醒 RunLoop 执行这个 source。 - Sources1 不仅包含了回调,另外包含了
mach_port_t (*getPort)(void *info);
可用于内核和其他线程相互发送消息,接收到消息时能够主动唤醒 RunLoop 的线程。
__CFRunLoopObserver
Observer 是观察者,每个 Observer 都包含一个回调 (函数指针)
typedef void (*CFRunLoopObserverCallBack)(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info);
当 RunLoop 的执行状态发生变化的时候,会通过回调返回执行状态
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入 RunLoop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timers
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Sources
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中被唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出 RunLoop
};
__CFRunLoopTimer
先来看一下 CFRunLoop.h 中对 __CFRunLoopTimer
的 typedef
typedef struct CF_BRIDGED_MUTABLE_TYPE(NSTimer) __CFRunLoopTimer * CFRunLoopTimerRef;
__CFRunLoopTimer
和 NSTimer
是 toll-free bridged 的,可以互相进行转换使用。其中包含一个时间点和一个回调,被加入 RunLoop 之后,当注册的时间点到时,RunLoop 会被唤醒以执行那个回调。
上述 Source/Observer/Timer 被统称为 mode item,mode 与 item 的关系为 1 对多的,但一个 item 被多次加入同一个 mode 时是无效的。
RunLoop 的运行
CoreFoundation 提供了两个运行 RunLoop 的方法
// 在 kCFRunLoopDefaultMode 下运行 RunLoop
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
// 在指定的 mode 下运行 RunLoop
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
摘录官方文档的介绍,RunLoop 的内部运行逻辑大致如下。
- Notify observers that the run loop has been entered.
- Notify observers that any ready timers are about to fire.
- Notify observers that any input sources that are not port based are about to fire.
- Fire any non-port-based input sources that are ready to fire.
- If a port-based input source is ready and waiting to fire, process the event immediately. Go to step 9.
- Notify observers that the thread is about to sleep.
- Put the thread to sleep until one of the following events occurs:
- An event arrives for a port-based input source.
- A timer fires.
- The timeout value set for the run loop expires.
- The run loop is explicitly woken up.
- Notify observers that the thread just woke up.
- Process the pending event.
- If a user-defined timer fired, process the timer event and restart the loop. Go to step 2.
- If an input source fired, deliver the event.
- If the run loop was explicitly woken up but has not yet timed out, restart the loop. Go to step 2.
- Notify observers that the run loop has exited.
简单整理翻译下,画个流程图
实际执行 RunLoop 的源码行数过多,直接看容易懵逼,所以删减一些,保留一些关键步骤加上一些注释。
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {
// 依据 modeName 获取 currentMode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
// currentMode 为空或 currentMode 中 sources/timers/observers 为空,直接 return
if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
Boolean did = false;
if (currentMode) __CFRunLoopModeUnlock(currentMode);
__CFRunLoopUnlock(rl);
return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
}
int32_t result = kCFRunLoopRunFinished;
// 1. Notify observers that the run loop has been entered.
// 通知 observer: RunLoop 即将进入
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
// 执行 RunLoop
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
return result;
// 10. Notify observers that the run loop has exited.
// 通知 observer: RunLoop 即将退出 kCFRunLoopExit
if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
return result;
}
// 运行 RunLoop
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
int32_t retVal = 0;
do {
// 2. Notify observers that any ready timers are about to fire.
// 通知 observer: 即将执行 Timers
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
// 3. Notify observers that any input sources that are not port based are about to fire.
// 通知 observer: 即将触发 Source0 回调
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
// 执行被加入的 Blocks
__CFRunLoopDoBlocks(rl, rlm);
// 4. Fire any non-port-based input sources that are ready to fire.
// RunLoop 触发 Source0 (非 port) 回调。
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
// 执行被加入的 Blocks
if (sourceHandledThisLoop) {
__CFRunLoopDoBlocks(rl, rlm);
}
// 5. If a port-based input source is ready and waiting to fire, process the event immediately. Go to step 9.
// 基于 mach_port 的事件处于 ready,立即处理 Source1,并跳转到 step 9。
if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
msg = (mach_msg_header_t *)msg_buffer;
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
goto handle_msg;
}
}
// 6. Notify observers that the thread is about to sleep.
// 通知 observer: RunLoop 的线程即将进入休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
// 7. Put the thread to sleep until one of the following events occurs:
// 1. An event arrives for a port-based input source.
// 2. A timer fires.
// 3. The timeout value set for the run loop expires.
// 4. The run loop is explicitly woken up.
// RunLoop 的线程进入休眠,直到下列事件中的一个事件发生时,再唤醒
// 1. 基于 mach port 的 Source1 事件
// 2. Timer 的时间到了
// 3. RunLoop 超时
// 4. 被其他方式主动唤醒
//
// __CFRunLoopServiceMachPort 内部调用 mach_msg 方法,等待接收 mach_port 的消息
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
// 8. Notify observers that the thread just woke up.
// 通知 observer: RunLoop 的线程刚刚被唤醒
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
// 处理消息
handle_msg;
// 9.1 到达 timer 设定的时间,触发 timer 回调
if (port == _timerPort) {
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())
}
// 9.2 __CFTSDKeyIsInGCDMainQ 如果 dispatch 到 GCD main_queue 的 block,执行它
else if (port == dispatchPort) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}
// 9.3 基于 mach_port 的 Source1 事件
else {
CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
mach_msg_header_t *reply = NULL;
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
if (NULL != reply) {
(void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
}
}
// 执行被加入的 Blocks
__CFRunLoopDoBlocks(rl, rlm);
if (sourceHandledThisLoop && stopAfterHandle) {
// 参数声明执行完毕就返回
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
// 超出参数要求的超时时间
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
// 外部调用强制停止
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
// 外部调用强制停止
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
// mode items 为空
retVal = kCFRunLoopRunFinished;
}
} while (0 == retVal);
return retVal
}
__CFRunLoopRun
方法内部维护了一个 do-while 循环。线程一直停留在这个循环中,直到超时或者被手动停止,函数才会返回。
基于 RunLoop 实现的功能
在平时的开发工作中,有许多功能的实现都是基于 RunLoop 实现的,例如 AutoReleasePool,事件响应,手势识别等等,这些后面日后再来补充吧。