简述
什么是 RunLoop?顾名思义 RunLoop 是一个运行循环,它的作用是使得程序在运行之后不会马上退出,保持运行状态,来处理一些触摸事件、定时器时间等。RunLoop 可以使得线程在有任务的时候处理任务,没有任务的时候休眠,以此来节省 CPU 资源,提高程序性能。
那 RunLoop 是怎样保持程序的运行状态,到底处理了哪些事件?下面我们就从源码的层面来了解一下 RunLoop。
RunLoop
获取 runloop 对象
NSRunLoop 和 CFRunLoopRef 都代表 RunLoop 对象,NSRunLoop 是对 CFRunLoopRef 的封装。
Foundation
[NSRunLoop currentRunLoop]; // 获得当前线程的 RunLoop 对象 [NSRunLoop mainRunLoop]; // 获得主线程的 RunLoop 对象
Core Foundation
CFRunLoopGetCurrent(); // 获得当前线程的 RunLoop 对象 CFRunLoopGetMain(); // 获得主线程的 RunLoop 对象
RunLoop 相关类
从源码的代码结构中我们可以找出来一下 5 个跟 RunLoop 相关的结构
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopObserverRef
CFRunLoopTimerRef
下面是 CFRunLoopRef 的结构代码
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread;
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
变量很多,我们不需要全部看,只需要注意这两个
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
每一个 runloop 里面有很多 mode(存在一个 set 集合里面),然后之后后一个 mode 叫做 currentMode,也就是说 runloop 一次只能处理一种 mode。
然后我们再看 CFRunLoopModeRef 的结构,我已经给大家省略了里面那些我们不需要关注的变量
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
CFStringRef _name;
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
};
根据上面这些我们大概的可以概括出来 RunLoop 这些相关类的关系。
CFRunLoopModeRef
由上面的源码我们可以稍微总结一下这个 CFRunLoopModeRef:
CFRunLoopModeRef 代表 RunLoop 的运行模式
一个 RunLoop 包含多个 CFRunLoopModeRef,每个 CFRunLoopModeRef 又包含多个_sources0,_sources1,_observers,_timers。
RunLoop 每次只能运行一种 mode,切换 mode 的时候,要先退出之前的 mode。
如果 mode 中没有_sources0、_sources1、_observers、_timers,程序会立刻退出。
常用的两种 Mode
kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App 的默认 Mode,通常主线程是在这个 Mode 下运行
UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
CFRunLoopObserverRef
源码中给出了可以监听的 RunLoop 状态
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
// 进入 RunLoop
kCFRunLoopEntry = (1UL << 0),
// 即将处理 timers
kCFRunLoopBeforeTimers = (1UL << 1),
// 即将处理 Sources
kCFRunLoopBeforeSources = (1UL << 2),
// 即将休眠
kCFRunLoopBeforeWaiting = (1UL << 5),
// 被唤醒
kCFRunLoopAfterWaiting = (1UL << 6),
// 退出循环
kCFRunLoopExit = (1UL << 7),
// 所有状态
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
具体的怎么样添加 observer 来监听 RunLoop 状态我就不贴代码了,网上一搜有很多的。
RunLoop 的运行逻辑
前面我们已经了解了 RunLoop 相关的结构的源码,知道了 RunLoop 大概的数据结构,那 RunLoop 到底是如何工作的呢?它的运行逻辑是什么?
我们了解过了每个 mode 中会存放不同的_sources0、_sources1、_observers、_timers,这些我们可以全部统称是 RunLoop 要处理的东西,那每一种具体对应我们了解的哪写事件呢?
Source0 触摸事件处理 performSelector:onThread:
Source1 基于系统 Port(端口) 的线程间通信系统事件捕捉
TimersNSTimer 定时器 performSelector:withObject:afterDelay:
Observers 用于监听 RunLoop 的状态 UI 刷新(BeforeWating)Autorelease Pool(BeforWaiting)
注:UI 的刷新并不是即时生效,比如说我们改变了 view 的 backgroundColor,当执行到这行代码是并不是立刻生效,而是先记录下有这么一个任务,然后在 RunLoop 处理完所有的时间,进入休眠之前 UI 刷新。
这是大神总结的 RunLoop 的运行逻辑图,我直接拿过来用了。我们主要是看左边这部分,右边的这些标注是在源码中对应的主要方法名称。
这个图很容易理解,只有从 06 跳转到 08 这一步,单从图上看的话不是很清晰,这一块结合源码就比较明了了。第 06 步,如果存在 Source1 就直接跳转到 08,在代码中使用了 goto 这个关键字,其实就是跳过了 runloop 休眠和唤醒这一部分的代码,直接跳转到了处理各种事件的这一部分。
下面我把源码做了一些删减,方便大家可以更清楚的梳理整个过程
// 这个是 runloop 入口函数
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {/* DOES CALLOUT */
// 通知 Observers 即将进入 RunLoop
if (currentMode->_observerMask & kCFRunLoopEntry) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
// 核心方法
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
// 通知 Observers 即将退出 RunLoop
if (currentMode->_observerMask & kCFRunLoopExit) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
return result;
}
下面是核心方法
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
int32_t retVal = 0;
do {
// 通知 Observers 即将处理 Timers
if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
// 通知 Observers 即将处理 Sources
if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
// 处理 Blocks
__CFRunLoopDoBlocks(rl, rlm);
// 处理 source0, 根据返回值决定在处理一次 blocks
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
if (sourceHandledThisLoop) {
__CFRunLoopDoBlocks(rl, rlm);
}
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
// source1 相关
if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
msg = (mach_msg_header_t *)msg_buffer;
// 是否有 Source1 有的话跳转到 handle_msg
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
goto handle_msg;
}
}
didDispatchPortLastTime = false;
// 通知 Observers: 即将休眠
if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
// 休眠
__CFRunLoopSetSleeping(rl);
// 等待别的消息来唤醒当前线程
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
__CFRunLoopUnsetSleeping(rl);
// 通知 Observers: 即将醒来
if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
// 标识标识 !!!!!
handle_msg:;
__CFRunLoopSetIgnoreWakeUps(rl);
// 下面根据是什么唤醒的 runloop 来分别处理
if (MACH_PORT_NULL == livePort) {
CFRUNLOOP_WAKEUP_FOR_NOTHING();
// handle nothing
} else if (livePort == rl->_wakeUpPort) {
CFRUNLOOP_WAKEUP_FOR_WAKEUP();
// do nothing on Mac OS
}
// 被 Timer 唤醒
else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
CFRUNLOOP_WAKEUP_FOR_TIMER();
// 处理 Timers
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
// Re-arm the next timer, because we apparently fired early
__CFArmNextTimerInMode(rlm, rl);
}
}
// 被 Timer 唤醒
else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
CFRUNLOOP_WAKEUP_FOR_TIMER();
// 处理 Timers
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
// Re-arm the next timer
__CFArmNextTimerInMode(rlm, rl);
}
}
// 被 GCD 唤醒
else if (livePort == dispatchPort) {
// 处理 GCD 相关
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else {
// 被 Source1 唤醒
// 处理 Source1
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
}
// 在处理一遍 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) {
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
retVal = kCFRunLoopRunFinished;
}
} while (0 == retVal);
return retVal;
}
图和源码结合来看,整个流程就清晰了很多。流程里面的有些东西不需要我们太过深入的研究,我们把这个流程掌握一下就 OK 了。
细节补充
第一点
我们都知道 RunLoop 有一个优势,那就是可以使线程在有工作的时候工作,没有工作的时候休眠,来减少占用 CPU 资源,提高程序性能。
这说明代码在执行到
// 等待别的消息来唤醒当前线程
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
的时候,会阻塞当前的线程。但这种阻塞跟我们之前所用到过的阻塞线程不是一回事。
举个例子,我们可以使用 while(1){}; 这句代码来阻塞线程,这句代码在底层会转换为汇编的代码,我们的线程一直在重读执行这几句代码,所以他仅仅是阻塞线程,并没有使线程休眠,我们的线程一直在工作。但是 runloop,通过 mach_msg 使用了一些内核层的 API,真的是实现了线程的休眠,让线程不再占用 CPU 资源。
第二点
RunLoop 与线程的关系?
一个线程对应一个 RunLoop 对象。
RunLoop 默认不创建,在第一次获取的时候创建,主线程中的默认存在 RunLoop 也是因为在底层代码中,提前获取过一次。
RunLoop 储存在一个全局的字典中,线程是 key,RunLoop 是 value。(源码中有所体现)
RunLoop 会在线程结束时销毁。