RunLoop 是 iOS 比较的核心之一,本篇用于梳理 RunLoop 相关的概念和底层实现。

什么是 RunLoop?

对于一个线程,一般情况下一次只执行一个任务,如果任务执行完成线程也会立刻退出。而对于一个 App,我们希望的是,即使 App 内没有任何的任务需要线程去处理了,但不能够让这些线程退出。即不能退出应用。这时就需要一种机制既能让线程能随时处理事件,且在处理完成后不退出。比如主线程,在我们启动应用之后就会一直存在,当有交互发生时,对应去处理相关的事件。RunLoop 就很好了满足了这个需求,它能够在没有收到消息的时候让线程休眠以避免资源浪费,当有消息来时会立刻唤醒线程处理。

这也是我们需要 RunLoop 的原因。

RunLoop 与线程之间的关系

苹果不允许直接创建 RunLoop,它提供了两个自动获取的函数:CFRunLoopGetMain()CFRunLoopGetCurrent()。CFRunLoop 是基于 pthread 来管理的。

// 全局的 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 里。第一次进入时,初始化全局 Dictionary,并先为主线程创建一个 RunLoop,将关系保存在 Dictionary 中。线程结束时销毁 RunLoop。我们只能在一个线程的内部获取其 RunLoop(主线程除外)。

NSRunLoop 和 CFRunLoopRef 的关系

NSRunLoop 是基于 CFRunLoopFef 封装。CGRunLoopRef 属于 CoreFoundation 框架,提供的是纯 c 函数的 API,这些 API 是线程安全的。NSRunLoop 提供的是面向对象的 API,这些 API 不是线程安全的。

RunLoop 的内部实现逻辑

RunLoop 的内部实现逻辑大致如下:

内部实现逻辑的代码如下:

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

RunLoop 的 Mode

开发过程中常遇到的一个场景是,在我们写 NSTimer 时为了避免滑动事件影响 NSTimer 的回调,我们通常会把 mode 置为 NSRunLoopCommonModes

这里的 NSRunLoopCommonModes 是什么?

先看下 CFRunLoopModeCFRunLoop 的结构:

struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};

struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
...
};

主线程的 RunLoop 里有两种 Mode:kCFRunLoopDefaultModeUITrackingRunLoopMode。这两种 Mode 都被标记为Common属性。App 平常所处的 mode 是 kCFRunLoopDefaultMode;当有滑动事件触发时,RunLoop 会切换 mode 到 UITrackingRunLoopMode

前面提到创建一个 Timer,默认会被加到 DefaultMode,正常情况下 Timer 会得到重复回调。但如果此时滑动一个 ScrollView,RunLoop 就会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被正常的回调。当然,此时 Timer 也不会影响到滑动操作。

到这里,我们大概知道了 RunLoop 系统预置的 model 有这几种:

  • NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认模式
  • UITrackingRunLoopMode:滑动模式
  • NSRunLoopCommonModes:通用模式,无论处于滑动或默认状态都能够响应实践

CommonModes

一个 Mode 可以通过将其 ModeName 添加到 RunLoop 的 commonModes 中,来将自己设置为 CommonMode。每当 RunLoop 的内容发生变化时,RunLoop 都会将事件同步到 CommonMode 的 mode item,什么是 mode item 后文会讲到。这也解释了为什么 timer 加入到 NSRunLoopCommonModes 中会被正确的回调。

在 NSRunLoop 这一层没有提供操作 Mode 的接口,在 CFRunLoopRef 对外提供了两个操作 Mode 的接口,只有增加 mode 的接口:

CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);

mode item

struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};

__CFRunLoopMode 结构体中的 _sources0_sources1_observers_timers 都属于 mode item。结构图如下:

  • Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。

  • Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程。

  • Timer 是基于时间的触发器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。

  • Observer 是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:

    typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
    kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
    kCFRunLoopExit = (1UL << 7), // 即将退出Loop
    };

一个 mode item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是无效的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。

RunLoop 启动和退出

启动

这边以 NSRunLoop 为例,CFRunLoopRef 类似, 启动有 3 个方法:

  • run
  • runUntilDate:
  • runMode:beforeDate:

run 底层是不断(循环)调用 runMode:beforeDate: 来达到运行目的。runUntilDate: 底层也是调用 runMode:beforeDate: 来运行,和 run 不同的是,在指定的时间也就是 UntilDate 参数到后会停止调用。

退出

在系统提供的停止 RunLoop 方法只有 CFRunLoopStop()CFRunLoopStop() 方法只会结束当前的 RunLoop 调用,而不会结束后续的调用。也就意味着 如果你是用方法一也就是 run 的方式启动 RunLoop,那么这个 RunLoop 不会被退出,因为它会不断的启动,因为 run 底层是不断(循环)调用 runMode:beforeDate: 来达到运行目的。如果你是使用 runUntilDate: 启动的,那么超时结束后会自动终止 RunLoop,如果是 runMode:beforeDate: 那么你可以精确的控制 RunLoop 的停止。

RunLoop 的应用

RunLoop 用途广泛,如 AutoreleasePool,PerformSelecter,GCD,AsyncDisplayKit 等都有涉及到。

参考文章:

评论