一直以来都听说过RunLoop这个东西,并且知道它是用来在某个线程中开启一种循环等待接受并处理事件状态的方法。然后好奇之下就查找了一些资料,然后发现了一篇大神对RunLoop的深入分析,看后大呼神奇!虽然看的不是很懂,好吧其实感觉看懂了皮毛而已,但是文章真的很不错,的确值得多看几次,细细品味!!!
作者:ibireme
文章链接:深入理解RunLoop
大家对这个作者有木有很熟悉啊!之前分享的一篇关于Rumtime机制的文章也是出自他之手!大神啊!= =。我要好好向大神学习。。。。
这篇文章对RunLoop进行了比较深入的分析,已经深入到了对底层源码分析了(亚历山大哈哈哈哈)。作者通过对底层实现的分析,介绍了苹果是如何利用RunLoop实现自动释放池、延迟回调、触摸事件、屏幕刷新等功能的实现的。
= =。以下都是我对该文章学习的一些总结。如果怕太水可以直接戳原文链接~~~
一般情况下,一个线程里的任务只能执行一次,一旦一个任务执行完了,线程就会退出并销毁。然而有时候我们需要某个线程能处于一种状态:线程需要处于一直运行状态,随时能接受事件并处理事件,并且在没有事件到达时可以暂时休眠以可以执行别的线程处理别的任务。
这种模型称之为Event Loop。作者举了个伪代码例子进行说明。
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
所以我们可以吧RunLoop看做是一个对象,它管理了需要处理的事件和消息。当一个RunLoop启动了之后,它就会进入上面那个do while循环中,不断的进行“接受消息->等待->处理消息”的过程,直到这个RunLoop被终止,函数返回。
OS X/iOS系统中提供了两个对象:NSRunLoop,CFRunLoopRef。
在说到iOS中的多线程时候,我们之前提到过有四种:pthread_t,NSThread,NSOperation,GCD。根据作者描述,苹果以前的文档中说NSThread只是对pthread_t的封装,
吐槽:= =。然后我猜测GCD也是基于pthread_t的,毕竟都是C语言实现的么哈哈哈哈,然后NSOperation是基于GCD的?
根据作者描述,pthread_t和NSThread是一一对应的(一个NSTread对象是对一个pthread_t创建的线程的管理?)。比如我们可以通过pthread_main_thread_np()
和[NSThread mainThread]
来获取主线程;也可以通过pthread_self()
和[NSThread currentThread]
来获取当前线程。而且CRunLoop是基于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对象是一一对应的。其保存的数据结构使用的是字典,当线程创建的时候并不存在RunLoop对象,如果不主动去获取该对象则不会产生该对象。并且RunLoop对象只会在第一次获取的时候产生,其销毁发生在线程结束的时候。PS:只能在一个线程的内部获取其RunLoop(主线程除外)。
Core Foundation中关于RunLoop的类主要有5个。
就是说,一个RunLoop可以包含若干个Mode(可以理解为一个RunLoop可以有多个运行模式?)。并且每个Mode又可以包含若干个Source,Timer,Observer。
每次调用RunLoop主函数时,只能指定为其中某一种Mode(成为当前模式currentMode)。如果需要切换Mode,则需要先退出Loop,然后重新指定进入新的Mode。= =。这样好像也是为了更好的隔离不同Mode下的Source,Timer,Observer,避免产生干扰。
(1)CFRunLoopSourceRef
是事件产生的地方。Source有两个版本:Source0 和 Source1。
Source0
只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。Source1
包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程。吐槽:我这里好像联想到GCD中的计时器实现,也是需要创建一个神马计时器事件源,然后设置事件源的处理handler,难道和这里有关联???传送门:iOS-多线程编程学习之GCD——线程组、延时、计时器等(六)
(2)CFRunLoopTimerRef
是基于时间的触发器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
(3)CFRunLoopObserverRef
是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。其中可以相应的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
};
上面提到的三个Source/Timer/Observer被统称为Mode Item(可以理解为模式下的三种监听事件组件?)。其中一个Mode Item可以被添加到多个Mode中。但是一个Mode Item重复添加到同一个Mode是没有意义的。PS:如果一个Mode中没有任何Mode Item,那么RunLoop会直接退出,不进入循环。
CRRunLoopMode和CFRunLoop的大致结构如下。
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
//...
};
CommonModes
:一个Mode可以把自己标记为”Common”属性(将将该Mode加入_commonModes集合中)。这样当RunLoop的内容发生变化时,RunLoop会将_commonModeItems集合中的所有Mode Item都加到标记为“Common”属性的Mode中。
这里举了个应用场景的例子:
主线程的RunLoop中more有两个预置的Mode:KFRunLoopDefaultMode和UITrackingRunLoopMode。这两个Mode都被标记为common的了。如果你创建一个Timer添加到DefaultMode后,Timer会被正常的重复回调,然而当RunLoop需要切换到UITrackingRunLoopMode处理UI事件时,这是Timer就不会被处理了。
所以如果你想要创建一个Timer可以在多种Mode下都可以被调用,那么第一种方法就是把Timer分别都加到两个Mode下。第二种方式则是把Timer加到_commonModeItems中,由于预置的两个模式都是被标记为common的,所以当切换Mode时,_commonModeItems的Mode Item都会被更新到对应的Mode中。
下面介绍一些苹果暴露出来的RunLoop相关管理的API接口。
RunLoop管理RunLoopMode。
//添加一个Mode,并且该Mode被标识为common
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
//切换RunLoop为某个指定的Mode?
CFRunLoopRunInMode(CFStringRef modeName, ...);
RunLoopMode管理Mode Item。
//添加Source
CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
//添加Obserser
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
//添加Timer
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
//移除Source
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
//移除Observer
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
//移除Timer
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
从上面的接口我们可以看出,我们如果需要操作某个Mode Item。则需要通过Mode name来进行,如果我们传入的Mode name不存在时,RunLoop会帮我们创建一个对应的CFRunLoopModeRef。
PS:对一个RunLoop来说,其内部的Mode只能增加不能删除。
= =。好吧这部分就是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就是在运行一段在do while中的代码,依次处理各种事件,并在适当时候发出通知。= =。当调用CFRunLoopRun()方法后,线程就会一直停留在这个循环中;知道线程超时或手动终止RunLoop,该函数才会返回。
上面的一段代码中应该大部分都好理解,其中应该会注意到其中有一个方法mach_msg
,该方法使得线程进入休眠状态;RunLoop的实现就是基于mach port这玩意的~
下面一段作者主要介绍可OS X/iOS系统架构。我就懒得多说了,详细的可以查资料或看作者原文了解一下。
这里主要的意思就是说在底层的实现中,在Darwin这个核心架构中,介于硬件之上有一个很重要的东西叫
Mach
。
XNU的内核被称作Mach,其是一个微内核,仅提供了诸如处理器调度,进程间通信(IPC)等少量服务。
在 Mach 中,所有的东西都是通过自己的对象实现的,进程、线程和虚拟内存都被称为”对象”。和其他架构不同, Mach 的对象间不能直接调用,只能通过消息传递的方式实现对象间的通信。”消息”是 Mach 中最基础的概念,消息在两个端口 (port) 之间传递,这就是 Mach 的 IPC (进程间通信) 的核心。
Mach 的消息定义是在<mach/message.h>
头文件的。
typedef struct {
mach_msg_header_t header;
mach_msg_body_t body;
} mach_msg_base_t;
typedef struct {
mach_msg_bits_t msgh_bits;
mach_msg_size_t msgh_size;
mach_port_t msgh_remote_port;
mach_port_t msgh_local_port;
mach_port_name_t msgh_voucher_port;
mach_msg_id_t msgh_id;
} mach_msg_header_t;
一条 Mach 消息实际上就是一个二进制数据包 (BLOB),其头部定义了当前端口 local_port 和目标端口 remote_port,
发送和接受消息是通过同一个 API 进行的,其 option 标记了消息传递的方向:
mach_msg_return_t mach_msg(
mach_msg_header_t *msg,
mach_msg_option_t option,
mach_msg_size_t send_size,
mach_msg_size_t rcv_size,
mach_port_name_t rcv_name,
mach_msg_timeout_t timeout,
mach_port_name_t notify);
为了实现消息的发送和接收,mach_msg() 函数实际上是调用了一个 Mach 陷阱 (trap),即函数mach_msg_trap(),陷阱这个概念在 Mach 中等同于系统调用。当你在用户态调用 mach_msg_trap() 时会触发陷阱机制,切换到内核态;内核态中内核实现的 mach_msg() 函数会完成实际的工作
RunLoop 的核心就是一个 mach_msg() (见上面代码的第7步),RunLoop 调用这个函数去接收消息,如果没有别人发送 port 消息过来,内核会将线程置于等待状态。例如你在模拟器里跑起一个 iOS 的 App,然后在 App 静止时点击暂停,你会看到主线程调用栈是停留在 mach_msg_trap() 这个地方。
作者在下面一段中介绍了App启动后RunLoop的状态,以及一些系统默认注册的Mode等。具体的可以查看作者原文。我们这里主要列出别的方面的常用功能和RunLoop的结合。
App启动之后,主线程的RunLoop里注册了两个Observer,其回调函数都是_wrapRunLoopWithAutoreleasePoolHandler()。
第一个Observer监听的事件是Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
第二个 Observer 监视了两个事件:
所以在主线程中执行的代码,通常是写在事件回调,Timer回调内的。这些会掉会被RunLoop创建好的AutoReleasePool环绕着,所以不会出现内存泄露,开发者也不必显示创建自动释放池了。
苹果注册了一个source1(基于mach port)用来接收系统事件,其回调函数为__IOHIDEventSystemClientQueueCallback()。
当一个硬件事件(锁屏/触屏/摇晃等)发生后,首先由IOKit.framework生成一个IOHIDEvent事件并由Springboard接收,随后用mach port转发给需要的App进程。随后苹果注册的那个Source1就会触发回调,并调用_UIApplicationHandleEventQueue()进行内部的分发。
_UIApplicationHandleEventQueue()会把IOHIDEvent处理并打包成UIEvent进行处理和分发。
当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。
苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。
当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。
当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。
苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。
NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。
如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。
CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉。Facebook 开源的 AsyncDisplayLink 就是为了解决界面卡顿的问题,其内部也用到了 RunLoop。
当调用 NSObject 的 performSelecter:afterDelay:
后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。
实际上 RunLoop 底层也会用到 GCD 的东西,比如 RunLoop 是用 dispatch_source_t 实现的 Timer(评论中有人提醒,NSTimer 是用了 XNU 内核的 mk_timer,我也仔细调试了一下,发现 NSTimer 确实是由 mk_timer 驱动,而非 GCD 驱动的)。但同时 GCD 提供的某些接口也用到了 RunLoop, 例如 dispatch_async()。
当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。
好了,就说这么多了。剩下的看心情我再决定更不更了,宝宝就是任性!