目录
一、什么是RunLoop
二、RunLoop和线程的关系
三、RunLoop的Mode
四、RunLoop的内部逻辑
一、什么是RunLoop
我们平常提到RunLoop主要指的是NSRunLoop
和CFRunLoop
,CFRunLoop
是CoreFoundation
框架下基于C语言的API,而NSRunLoop
则是Foundation
框架下对CFRunLoop
的OC封装,并未提供额外的其他功能,所以:
RunLoop其实也是一个NSRunLoop
的OC对象,它的Mode里包含了一个线程需要处理的各种事件,同时也提供了一个至关重要__CFRunLoopRun
函数来完成我们通常意义所说的跑圈(跑圈就是指当有事件需要处理时线程处理事件,没有事件需要处理时线程就休眠,当有事件来临时线程被唤醒处理事件,如此循环),这个函数其实就是RunLoop的核心所在,RunLoop的核心其实就是(一个do-while循环 + 线程休眠/唤醒机制),而线程休眠/唤醒机制则是RunLoop的精髓所在,是它区别于其它框架EventLoop而独特的地方,我们会在第三部分和第四部分详细介绍这两点内容。
接下来我们会跳过NSRunLoop
,而是直接从CFRunLoop
的源码下手,从底层上来了解一下RunLoop。
二、RunLoop和线程的关系
在iOS开发中我们会遇到两个线程对象:pthead和NSThread,pthead是基于C语言的API,而NSThread则是pthead的OC封装,它们俩是一一对应的。
而我们之所以要提到RunLoop和线程的关系,是因为RunLoop是基于pthead来管理的,具体的说,苹果并没有为我们提供直接创建RunLoop的API,开发中如果我们想要使用RunLoop,就只能通过苹果提供的CFRunLoopGetMain()
和CFRunLoopGetCurrent()
函数来获取一个RunLoop,而函数的内部我们其实传入了一个线程。这两个函数的内部逻辑大概如下:
// 全局区runLoopDict,用来存放Runloop,其中key就是pthead,对应的value就是Runloop
static CFMutableDictionaryRef runLoopDict;
// 获取一个线程所对应的Runloop
CFRunLoopRef _CFRunLoopGet(pthread thread) {
// 第一次启动App
if (!runLoopDict) {
// 初始化全局区runLoopDict
runLoopDict = CFDictionaryCreateMutable();
// 为主线程创建一个Runloop,并把主线程和主Runloop作为一对key-value保存到runloopDict中
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
CFDictionarySetValue(runLoopDict, pthread_main_thread_np(), mainLoop);
}
// 从runloopDict中获取指定线程所对应的Runloop
CFRunLoopRef runloop = CFDictionaryGetValue(runLoopDict, thread);
// 如果获取不到指定线程所对的Runloop
if (!runloop) {
// 就为该线程创建一个Runloop
CFRunLoopRef newRunloop = __CFRunLoopCreate(thread);
// 保存在全局区runLoopDict中
CFDictionarySetValue(runLoopDict, thread, newRunloop);
// 并返回
runloop = CFDictionaryGetValue(runLoopDict, thread);
// 同时注册一个回调,确保当线程销毁的时,顺便也销毁其对应的Runloop
_CFSetTSD(..., thread, runloop, __CFFinalizeRunLoop);
}
return loop;
}
从上面的代码我们可以的出以下结论:
-
RunLoop是基于线程来管理的,它们一一对应,共同存储在一个全局区的runLoopDict中,线程是key,RunLoop是value。
-
RunLoop的创建:主线程所对应RunLoop在程序一启动创建主线程的时候系统就会自动为我们创建好,而子线程所对应的RunLoop并不是在子线程创建出来的时候就创建好的,而是在我们获取该子线程所对应的RunLoop时才创建出来的,换句话说,如果你不获取一个子线程的RunLoop,那么它的RunLoop就永远不会被创建。
-
RunLoop的获取:我们可以通过一个指定的线程从runLoopDict中获取它所对应的RunLoop。
-
RunLoop的销毁:系统在创建RunLoop的时候,会注册一个回调,确保线程在销毁的同时,也销毁掉其对应的RunLoop。
三、RunLoop的Mode
谈到RunLoop,我们就不得不谈它的Mode,RunLoopMode是个非常重要的概念。这一部分我们会先谈一下RunLoop和RunLoopMode的关系,然后再单独谈一下RunLoopMode。
1、RunLoop和RunLoopMode的关系
我们先来看下RunLoop和RunLoopMode的关系。
struct __CFRunLoop {
CFMutableSetRef _commonModes;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
...
};
-
系统一共为RunLoop提供了两种不同的RunLoopMode(两种模式以外我们可以自定义RunLoopMode),即DefaultMode和TrackingMode,DefaultMode是RunLoop默认的运行模式,而TrackingMode是RunLoop追踪scrollView滚动时的运行模式。
比如说我们启动App后什么也不做,那么主线程对应的RunLoop就默认运行在DefaultMode下,此时如果我们创建一个timer(timer会被默认添加到RunLoop的DefaultMode下),timer事件会被正常触发,但如果此时我们滑动了一个scrollView,那么主线程的RunLoop就会切换到TrackingMode模式下,timer事件就无法正常被触发了。
因此如果我们想要达到滚动scrollView时,timer事件也能被正常触发,一种办法就是创建timer的时候把它分别添加到RunLoop的DefaultMode和TrackingMode下,另一种办法则是把timer添加到RunLoop的CommonModes下。严格来说,CommonModes其实不是RunLoop一种Mode,而是一些Mode的组合,比如说系统就默认的把DefaultMode和TrackingMode添加到这种模式中了,也就是说如果我们让一个RunLoop运行在CommonModes下,并不是说RunLoop就真得是运行在CommonModes下,只是说RunLoop在切换Mode的时候会自动把原Mode的Timer/Source/Observer自动同步到目标Mode下,从而保证事件源在所有的Mode下都能正常触发。
-
每一个RunLoop都必须运行在某种特定的RunLoopMode下,所以在RunLoop的主函数
RunLoopRun
每次被触发之前,我们都得给当前RunLoop指定一个它要运行的RunLoopMode,也就是_currentMode
成员变量,RunLoop默认运行在DefaultMode下。但这并不是一个RunLoop就只能运行一种Mode,只是同一时间只能运行一种Mode而已,比如说它的modes
成员变量里存储着DefaultMode和TrackingMode,则表明RunLoop可以运行在这两种模式下,默认的时候运行DefaultMode,当滚动scrollview的时候则运行在TrackingMode下。 -
当我们要切换一个RunLoop的Mode时,必须退出当前Mode,然后重新进入RunLoop,以确保不同Mode之间的事件源互不影响。
2、RunLoopMode
上一小节我们说完了RunLoop和RunLoopMode的关系,这一小节我们单独来看下RunLoopMode。
struct __CFRunLoopMode {
CFStringRef _name;
CFMutableArrayRef _timers;
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
...
};
一个RunLoopMode内部又包含Source、Timer、Observer三种、若干个mode item,Timer和Source是RunLoop的事件源,Observer是RunLoop的观察者。
- Source事件源
Source事件源可以分为:Source0和Source1。
Source0事件源(非基于port),主要一些负责App内部事件的处理(比如我们比如改变了一个view的frame或者手动调用了 UIView/CALayer的setNeedsLayout/setNeedsDisplay方法时,这个布局事件就是Source0事件,会被标记为待处理,在下一个RunLoop执行时会被处理掉。),它只包含了一个回调,不能主动唤醒线程,需要先调用CFRunLoopSourceSignal(source)
,将这个Source标记为待处理,然后手动调用CFRunLoopWakeUp(runloop)
来唤醒RunLoop的线程去执行相应的事件。
Source1事件源(基于port),主要用来处理硬件事件(例如触摸、手势、手机锁屏、静音、靠近传感器、屏幕旋转、加速感应等事件),包含了一个mach_port和一个回调,它能主动唤醒线程来执行事件。
- Timer事件源
Timer事件源和Source1事件源一样,是一种基于port的事件源,主要负责的就是NSTimer事件。NSTimer就是基于RunLoop在工作的,因此我们在使用NSTimer的时候必须得把它添加到一个特定的RunLoop中,否则timer是起不了作用的,只要我们把timer添加到RunLoop中,RunLoop就会注册相应的时间点,一到注册的时间点,线程就会被唤醒去执行Timer事件。
-
Observer观察者
Observer不是RunLoop的事件源,而是RunLoop的观察者,它主要用来监听RunLoop的运行状态。Observer内部有一个回调,每当RunLoop的运行状态发生变化时,Observer的这个回调就会被触发,从而接收到RunLoop运行状态的变化告知外界。RunLoop的主要运行状态有如下几个:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),// 即将进入RunLoop
kCFRunLoopBeforeTimers = (1UL << 1),// 即将处理Timer事件
kCFRunLoopBeforeSources = (1UL << 2),// 即将处理Source事件
kCFRunLoopBeforeWaiting = (1UL << 5),// 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6),// 即将从休眠中唤醒
kCFRunLoopExit = (1UL << 7),// 即将退出RunLoop
};
四、RunLoop的内部逻辑
一个线程创建成功后,如果它对应的RunLoop也被创建成功,那么系统就会调用RunLoop的__CFRunLoopRun
函数使RunLoop对象开始运行,也就是进入我们通常意义上所说的跑圈,这个函数就是RunLoop的核心所在,它的伪代码如下:
int __CFRunLoopRun(runloop, currentMode, timeout, ...) {
// 1、将要进入RunLoop,并通知RunLoop的Observers
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
int exit = 0;
do {
// 2、RunLoop的线程将要处理Timer事件,并通知RunLoop的Observers
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
// 3、RunLoop的线程将要处理Source0事件,并通知RunLoop的Observers
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
// 4、RunLoop的线程处理非延迟的主线程调用
__CFRunLoopDoBlocks(runloop, currentMode);
// 5、RunLoop的线程处理Source0事件
__CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
// 6、处理完Source0事件之后,检测一下有没有Source1事件处于待处理状态,如果有就直接跳转到第10步的消息唤醒机制那里去处理Source1事件(因为Source1事件是基于port的,所以跳转到那里去处理了)
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
if (hasMsg) goto handle_msg;
}
// 7、Source0事件处理完之后没有检测到Source1事件,或者Source0事件处理完之后检测到Source1事件并且处理完了Source1事件,那么就表明此时已经没有Source事件需要处理了,通知观察者RunLoop的线程即将进入休眠
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
}
// 8、与此同时,RunLoop会调用系统内核Mach的方法mach_msg()函数,来等待一个唤醒,然后线程会进入休眠,直到被被唤醒
mach_port wakeUpPort = mach_msg(msg, MACH_RCV_MSG, port);
// 9、收到唤醒事件,RunLoop的线程被唤醒,并通知RunLoop的Observers,
__CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
// 10、处理唤醒事件:包括手动唤醒事件(如Source0唤醒)和基于port的唤醒事件(如Timer唤醒、Source1唤醒、异步唤醒)
if (wakeUpPort == TimerPort) {// 处理Timer唤醒事件
__CFRunLoopDoTimers();
} else if (wakeUpPort == Source1Port) {// 处理Source1唤醒事件
_CFRunLoopDoSource1();
} else {// 处理子线程dispatch到主线程的事件
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}
// 11、退出RunLoop有三种情况
if (__CFRunLoopIsStopped(runloop)) {// RunLoop被外部调用者强行终止了
exit = 1;
} else if (timeout) {// RunLoop超时了
exit = 1;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {// RunLoop当前的Mode下没有Source/Timer/Observer了
exit = 1;
}
} while (exit == 0);// 如果RunLoop不满足退出的条件,就继续循环
// 12. 通知Observers:RunLoop 即将退出。
__CFRunLoopDoObservers(kCFRunLoopExit);
}
__CFRunLoopRun
方法的内部逻辑也可以用下图来描述:

参考博客 1 : 深入理解 RunLoop
参考博客 2 : iOS刨根问底 - 深入理解 RunLoop
网友评论