今天来聊聊RunLoop,我们应该都知道RunLoop是运行循环的意思,有事情就去做事情,没事情就休息。那么问题来了,需要做的事情有哪些?休息的时候又是怎么休息的?在实际操作中我们应该怎么使用呢?
RunLoop是什么?
首先,一个线程一次只能执行一个任务,执行完成后线程就会销毁。但是我们需要一个机制,让线程随时处理事件并不销毁,那就要考虑设立一个线程观察者对象,这个对象在接收到消息以后就去处理事情,没有事情的时候又不占用资源进入休眠状态,在消息到来时再次被唤醒。其实在各个系统中都有这样的机制,可能叫法不一,但意思相似,而iOS中,它就叫做RunLoop。
所以,RunLoop实际上就是一个对象。在iOS中,程序启动,便自动创建一个和主线程对应的RunLoop。这个对象管理了其需要处理的事件和消息,并提供了一个入口函数,线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束,函数返回。
RunLoop和线程的关系
首先来看一段源码
/// 全局的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 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop的销毁是发生在线程结束时。你只能在一个线程的内部获取其RunLoop(主线程除外)。
苹果不允许直接创建RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。
RunLoop的结构
在CoreFoundation里面关于RunLoop有5个类:
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef 其中CFRunLoopModeRef类并没有对外暴露,只是通过CFRunLoopRef的接口进行了封装。
再来看一下iOS中RunLoop的结构图:
可以看到这中间的关系是:一个RunLoop中包含很多Mode,一个Mode里面又包含一系列Soure/Observer/Timer。
每次调用RunLoop的主函数时,只能指定其中一个Mode,这个Mode被称作CurrentMode。如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
Source/Timer/Observer 被统称为mode item,一个item可以被同时加入多个mode。但一个item被重复加入同一个mode时是不会有效果的。如果一个mode中一个item都没有,则 RunLoop会直接退出,不进入循环。
看一段源码:
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
...
};
下面分别介绍一下_modes和_commonModeItems。
_modes
系统默认提供五个Mode,分别是以下:
-
kCFRunLoopDefaultMode:默认的运行模式,通常主线程是在这个Mode下运行。
-
UITrackingRunLoopMode:用于跟踪触摸事件触发的模式(例如UIScrollView上下滚动)主线程当触摸事件触发会设置为这个模式,可以用来在控件事件触发过程中设置Timer。
-
UIInitializationRunLoopMode:在刚启动App时第进入的第一个Mode,启动完成后就不再使用。
-
GSEventReceiveRunLoopMode:用于接受系统事件,属于内部的RunLoop模式。
-
kCFRunLoopCommonModes: 贴上YYKit作者博客的原话:
是一组常用的模式集合(Set),一个mode可以将自己标记为“commons”(通过将其ModeName添加到RunLoop的 “commonModes” 中)。每当RunLoop的内容发生变化时,RunLoop都会自动将 _commonModeItems里的Source/Observer/Timer同步到具有 “Common” 标记的所有Mode里。
这段话似乎不太好理解,举个列子:
假如我有个Timer要关联到这些模式上,一个个注册很麻烦,我可以用:CFRunLoopAddCommonMode([[NSRunLoop currentRunLoop] getCFRunLoop],(__bridge CFStringRef) UITrackingRunLoopMode); 将UITrackingRunLoopMode或者其他模式添加到这个kCFRunLoopCommonModes模式中,然后只需要将Timer关联到kCFRunLoopCommonModes,即可以实现RunLoop运行在这个模式集合中任意一个模式时,这个Timer都可以触发。
当然,主线程的RunLoop里默认包含了kCFRunLoopDefaultMode和UITrackingRunLoopMode。
_commonModeItems
1.Source
Source有两个版本:Source0 和 Source1。
-
Source1:基于mach_Port的,来自系统内核或者其他进程或线程的事件,可以主动唤醒休眠中的RunLoop(iOS里进程间通信开发过程中我们一般不主动使用)。mach_port大家就理解成进程间相互发送消息的一种机制就好。
-
Source0:非基于Port的处理事件,什么叫非基于Port的呢?就是说你这个消息不是其他进程或者内核直接发送给你的。
简单举个例子:一个APP在前台静止着,此时,用户用手指点击了一下APP界面,那么过程就是下面这样的:
我们触摸屏幕,先摸到硬件(屏幕),屏幕表面的事件会先包装成Event,Event先告诉source1(mach_port),source1唤醒RunLoop,然后将事件Event分发给source0,然后由source0来处理。
如果没有事件,也没有timer,则runloop就会睡眠,如果有,则runloop就会被唤醒,然后跑一圈。
2.Timer
CFRunLoopTimerRef是基于时间的触发器,它和 NSTimer 是toll-free bridged(能够在Core Foundation和Foundation之间互换使用,可被互换使用的数据类型) 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
注释:Timer,source0,source1是常见的几个RunLoop事件源,还有一个GCD也可以算是一个(就是runloop内部循环时,会去询问GCD有没有事件需要我来帮你处理,如果有,我就帮你处理一下,没有就算了)
3.Observer
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
};
小结:
- RunLoop是个对象,与线程一一对应,里面包含了一系列的Mode(5种),而Mode又包含了一系列的modeItem,而modeItem又是由Soure/Timer/Observer组成。
- RunLoop运行时只能以一种固定的模式运行,如果需要切换Mode,只能退出当前的循环,再重新指定一个Mode进入。
- 运行时它只会监控这个模式下添加的modeItems,如果这个模式下没有相应的事件源,RunLoop的运行也会立刻返回的。
RunLoop的『生命周期』
根据前文的叙述,我们大概都知道了RunLoop的结构,以及组成RunLoop的各部分分别负责什么工作。那么接下来我们看RunLoop的工作流程应该就比较容易理解了。
首先来看一下工作流程图:
如果不好理解,举个例子来解释一下上述的工作流程:
- 应用主线程启动,系统默认创建与之对应的RunLoop,并最终保持在kCFRunLoopDefaultMode,此时,启动一个定时器10秒后开始执行,RunLoop处于Sleep状态
- 5秒的时候,用户点击了界面(source1到source0)唤醒了RunLoop。
- RunLoop通知observers即将触发timer回调度,通知observer即将触发Source0回调,执行被加入到Runloop Block链的Block。
- 执行用户点击事件(source0),并执行被加入到Runloop Block链的Block。
- 判断这过程中是否有source1事件发生。
- 如果有source1事件,去执行source1,执行完后判断是否符合退出Runloop的条件,符合退出Runloop,否则继续RunLoop。
- 如果没有,进入sleep状态。
- 10秒钟的时候,Timer触发,通知observers,RunLoop被唤醒了。
- 执行timer事件,执行被加入到Runloop Block链的Block。
- 判断是否符合退出RunLoop的条件,符合退出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的底层原理
从上面的源码可以看出,其中有2个东西值得我们注意:mach port和mach_msg()。RunLoop的核心是基于mach port的,其进入休眠时调用的函数是mach_msg(),接下来解释一下这两个东西是什么。
mach是一个内核,提供cpu调度,进程间通信等一些基础服务,在 Mach 中,所有的东西都是通过自己的对象实现的,进程、线程和虚拟内存都被称为”对 象”。和其他架构不同, Mach 的对象间不能直接调用,只能通过消息传递的方式实现对象间的通信。”消息”是 Mach 中最基础的概念,一条消息中包含当前端口 local_port 和目标端口 remote_port,消息在两个端口 (port) 之间传递,这就是 Mach 的 IPC (进程间通信) 的核心。消息的发送和接收使用<mach/message.h>中的mach_msg()函数,而mach_msg()的本质是一个调用mach_msg_trap(),这相当于一个系统调用,会触发内核状态切换。让程序处于休眠状态。而这个状态就是:
APP依然在内存中,但不在主动申请cpu资源(此处不要较真,肯定不是完全不用cpu的),然后一直监听某一个端口(port),等待内核向该端口发送消息,监听到消息后,从睡眠状态中恢复(重新启动RunLoop的循环,处理完事件后继续休眠)。
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,下一个符号断点mach_msg_trap,能够验证是走的该函数。
tips:source0,source1,timer这些事件源到底是什么关系,不太懂?
RunLoop从两种不同类型的Sources 接受事件(注意,这里是sources是事件源的意思,不要理解成source0或source1),Input Sources 传递异步事件,通常是来自另一线程或来自不同应用程序的消息。Timer Sources传递在特定的时间或者重复的间隔内发生的同步事件。这两种类型的源都使用特定于应用程序的处理程序来处理事件到达时的事件。
Input sources 异步地向线程传递事件。事件的来源取决于输入源的类型,通常是两种类型之一。基于端口的输入源监控应用程序的Mach端口。自定义输入源监视事件的自定义来源。就运行循环而言,输入源是基于端口的还是自定义的都不重要。系统通常实现两种类型的输入源,它们可以按原样使用。这两个来源之间唯一的区别是它们是如何发出信号的。基于端口的源代码由内核自动发出,并且自定义源必须手动从另一个线程发出信号。
RunLoop的实际应用
1.NSTimer
NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00,10:10,10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。
如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。
2.AutoreleasePool
App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。
第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。
关于其他更多关于AutoreleasePool的知识可以翻看其他博主的博客,推荐一篇。
3.事件响应
苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为
__IOHIDEventSystemClientQueueCallback()。当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。这个过程的详细情况可以参考这里。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用
_UIApplicationHandleEventQueue()进行应用内部的分发。
_UIApplicationHandleEventQueue()会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。
4.手势识别
当上面的
_UIApplicationHandleEventQueue()识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是
_UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。
5.界面更新
当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的
setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。
6.PerformSelecter
当调用 NSObject 的
performSelecter:afterDelay:后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。
7.NSURLConnection
通常使用 NSURLConnection 时,你会传入一个 Delegate,当调用了 [connection start] 后,这个 Delegate 就会不停收到事件回调。实际上,start 这个函数的内部会会获取 CurrentRunLoop,然后在其中的 DefaultMode 添加了4个 Source0 (即需要手动触发的Source)。CFMultiplexerSource 是负责各种 Delegate 回调的,CFHTTPCookieStorage 是处理各种 Cookie 的。
当开始网络传输时,我们可以看到 NSURLConnection 创建了两个新线程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader 这个线程内部会使用 RunLoop 来接收底层 socket 的事件,并通过之前添加的 Source0 通知到上层的 Delegate。
NSURLConnectionLoader 中的 RunLoop 通过一些基于 mach port 的 Source 接收来自底层 CFSocket 的通知。当收到通知后,其会在合适的时机向 CFMultiplexerSource 等 Source0 发送通知,同时唤醒 Delegate 线程的 RunLoop 来让其处理这些通知。CFMultiplexerSource 会在 Delegate 线程的 RunLoop 对 Delegate 执行实际的回调。
总结
通过本篇,我们要知道:
- RunLoop是一个对象,他与线程有着一一对应的关系。
- RunLoop包含了一系列Mode,Mode又包含了一系列ModeItems,ModeItems又是由Source/Timer/Observer组成。
- Mode常用的有5种,分别处理不同的事物。其中Commons比较特殊,是一个集合,你可以将其他的Mode通过方法添加进Commons,这样就可以减少重复关联到Mode。
- RunLoop运行时只能以一种固定的模式运行,如果需要切换Mode,只能退出当前的循环,再重新指定一个Mode进入
- Soure有2类,分别是Source1和Source0,Source1处理来自系统内核或者其他进程或线程的事件,Source0处理不是其他进程或者内核直接发送给你的事件。
- APP启动的时候主线程RunLoop跟着启动,并且默认包含kCFRunLoopDefaultMode和UITrackingRunLoopMode,Observer能监听定时器和各种交互事件以及界面刷新,当事件到来的时候会通过发送通知的方式触发事件处理,将事件block丢到block处理队列。事情处理完毕以后会进入休眠,等待下一次的唤醒,如果有符合退出RunLoop的条件,退出runloop,不符合继续runloop循环。
- RunLoop的底层是通过基于mach port在内核和线程间进行消息发送实现的,为了实现消息的发送和接收,mach_msg() 函数实际上是调用函数mach_msg_trap()。
- RunLoop在定时器,自动释放池,线程间通信,事件的响应和UI的刷新等方面都有应用。