一、Runloop的概念
Runloop是维护事件循环来对事件/消息进行管理的对象,runloop提供了一个函数,线程只要执行了这个函数,就会一直处于这个函数内部 "接受消息->等待->处理" 的循环中,没有消息需要处理时,runloop就休眠,不占用CPU,有消息处理时,runloop就立刻被唤醒。
总结:runloop(死循环)保证runloop所在的线程不退出
二、为什么要有runloop?
1、让线程一直活着接受用户输入
2、决定线程在什么时候应该处理哪些事件
3、发送事件的一方不能被接受事件的一方卡住,他会连续发送事件,所以要有消息队列
4、没有事件时,线程会处于阻塞态,不占用CPU,从而节省CPU时间
- CFRunLoopRef是在Core Foundation框架里的,它提供了纯C函数的API,所有这些API都是线程安全的
- NSRunLoop是CFRunLoopRef的封装,提供了面向对象的API,这些API都不是线程安全的。
三、数据结构
__CFRunLoopRun内部其实是一个_do while_循环,这也正是Runloop运行的本质。执行了这个函数以后就一直处于"等待-处理"的循环之中,直到循环结束。只是不同于我们自己写的循环它在休眠时几乎不会占用系统资源,当然这是由于系统内核负责实现的,也是Runloop精华所在。
CFRunLoopRef和CFRunloopMode、CFRunLoopSourceRef/CFRunloopTimerRef/CFRunLoopObserverRef关系如下图:


一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
1、Runloop Mode
- 默认模式(NSDefaultRunLoopMode):一般处理timer、网络等事件。
- UI模式(优先级最高)(UITrackingRunLoopMode):专门处理UI事件,只有触摸事件才能唤醒UI模式,当UI模式和默认模式同时有事件发生时,runloop会去处理UI模式下的事件
- UI模式 + 默认模式 (NSRunLoopCommonModes):占位模式,在iOS系统中默认包含了NSDefaultRunLoopMode和 UITrackingRunLoopMode(注意:并不是说Runloop会运行在kCFRunLoopCommonModes这种模式下,而是相当于分别注册了 NSDefaultRunLoopMode和 UITrackingRunLoopMode。
2、Source
Source结构体类型:事件源,任意事件本质都是source,source是RunLoop的数据源抽象类
按照函数调用栈分为
1、source0:非source1就是source0,负责App内部事件,由App负责管理触发,例如UITouch事件,Source0是Input Source中的一类,Input Source还包括Custom Input Source,由其他线程手动发出,RunLoop被这些事件唤醒之后就会处理并调用事件处理方法(CFRunLoopTimerRef的回调指针和CFRunLoopSourceRef均包含对应的回调指针)。
2、source1:系统内核事件的处理,Source1可以监听系统端口和其他线程相互发送消息,它能够主动唤醒RunLoop(由操作系统内核进行管理,例如CFMessagePort消息),source0不能主动唤醒runloop。
3、Observer
observer:观察runloop,向外部报告runloop的几种状态(它包含一个函数指针_callout_将当前状态及时告诉观察者),框架中的很多机制都是由RunLoopObserver触发,比如CAAnimation,CAAnimation是在runloop要睡的时候调用,把这一圈的动画事件全部汇集起来,最后去调用。

4、timer
5、Call out
在开发过程中几乎所有的操作都是通过Call out进行回调的(无论是Observer的状态通知还是Timer、Source的处理),而系统在回调时通常使用如下几个函数进行回调(换句话说你的代码其实最终都是通过下面几个函数来负责调用的,即使你自己监听Observer也会先调用下面的函数然后间接通知你,所以在调用堆栈中经常看到这些函数):
objectivec
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FOUNDTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM__FUNCTION__
6、RunLoop休眠
其实对于Event Loop而言RunLoop最核心的事情就是保证线程在没有消息时休眠以避免占用系统资源,有消息时能够及时唤醒。RunLoop的这个机制完全依靠系统内核来完成,
当程序静止时,RunLoop停留在__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy),而这个函数内部就是调用了mach_msg(本质上是一个系统调用)让程序处于休眠状态。
四、Runloop运行流程

一、核心执行阶段
1. 前置通知 + 高优先级事件处理(先干活,再休眠)
- 第一步:通知观察者 "准备处理事件" 先给当前 Mode 的所有
Observer发通知(如kCFRunLoopBeforeWaiting),让外部框架(如 UIKit)提前准备(比如刷新界面状态)。 - 第二步:优先处理 "到期 Timer" 只触发当前 Mode 下已到时间的
Timer(如NSTimer),未到期的留到后续迭代。 - 第三步:处理 "就绪 Source0" 执行用户态事件(如
performSelector任务、UI 点击预处理),但Source0不能主动唤醒 RunLoop,需靠其他事件(如Source1)唤醒后顺带处理。
2. 内核事件检查(Source1 是 "紧急唤醒键")
- 检查当前 Mode 的
Source1(内核态事件,如跨线程消息、系统端口消息)是否就绪:- 若就绪 :立即执行其回调,处理完后必须 "回退" 到 "处理 Timer" 步骤(避免遗漏因
Source1唤醒后新到期的 Timer / 新就绪的 Source0); - 若未就绪:进入休眠流程。
- 若就绪 :立即执行其回调,处理完后必须 "回退" 到 "处理 Timer" 步骤(避免遗漏因
3. 休眠 - 唤醒 - 定向处理(无活就休眠,被唤醒就精准干活)
- 休眠前:通知观察者 "要休眠" 发
kCFRunLoopBeforeWaiting通知,告知外部 "RunLoop 即将释放 CPU 休眠"。 - 休眠中:等待被唤醒 线程暂停,仅被 4 类事件唤醒:新
Source0就绪(需手动CFRunLoopWakeUp)、Source1触发、Timer到期、外部CFRunLoopStop。 - 唤醒后:先通知,再定向处理
- 发
kCFRunLoopAfterWaiting通知,告知 "已唤醒"; - 按 "唤醒原因" 精准处理:
- 因
Timer/Source1唤醒:回退到 "处理 Timer" 步骤,重新扫事件; - 因
Source0唤醒:直接去 "处理 Source0"; - 因
CFRunLoopStop唤醒:标记 "退出",结束本次迭代。
- 因
- 发
二、循环判断:是否继续下一轮迭代
- 每次处理完事件后,检查两个退出条件:
- 是否收到
CFRunLoopStop信号; - 当前 Mode 下是否无任何可处理的
Source/Timer。
- 是否收到
- 满足任一条件:停止 RunLoop;
- 不满足:回到 "前置通知" 步骤,开始下一次迭代。
五、Runloop与NSTimer
当触摸事件发生时,timer事件不显示,因为把timer添加到默认模式了,解决办法是把timer添加到commonMode模式。
六、Runloop与线程的关系
- 线程是和RunLoop一一对应的。
- 自己创建的线程默认是没有RunLoop的。
- 主线程的RunLoop默认是开启的。
问题:怎么样实现一个常驻线程
- 为当前线程开启一个RunLoop、
- 向该RunLoop中添加一个Port/Source等维持RunLoop的事件循环。
- 启动该RunLoop。
1、RunLoopObserver和Autorealease Pool的关系
UIKit通过RunLoopObserver在RunLoop两次Sleep间 (runloop的一圈)对AutoreleasePool进行Pop和Push将这次Loop中产生的Autorelease对象释放,把自动释放池中的对象进行释放。
2、UI更新
当UI改变( Frame变化、 UIView/CALayer 的继承结构变化等)时,或手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理。
苹果注册了一个用来监听BeforeWaiting和Exit的Observer,在它的回调函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。