RunLoop 内部逻辑(六步流程)
RunLoop 的核心逻辑主要是在 CoreFoundation 框架下的 CFRunLoop.c 文件中的 __CFRunLoopRun 函数里实现的。
虽然网上常说“六步”,但严格对应源码来看,其内部逻辑更为细致,通常分为 10 个关键节点。为了方便理解,我们可以将其归纳为几个大的阶段。
以下是 RunLoop 内部逻辑的详细流程解析:
核心流程图解
RunLoop 的本质是一个 do...while 循环,其核心在于“接收消息 -> 等待 -> 处理”。
第一阶段:准备与通知 (Entry & Pre-check)
通知 Observers:即将进入 Loop
- 状态:
kCFRunLoopEntry - 说明:告诉观察者,RunLoop 启动了。
- 状态:
通知 Observers:即将处理 Timers
- 状态:
kCFRunLoopBeforeTimers - 说明:通知观察者即将处理定时器回调。
- 状态:
通知 Observers:即将处理 Sources
- 状态:
kCFRunLoopBeforeSources - 说明:通知观察者即将处理输入源(非基于 Port 的 Source0)。
- 状态:
处理 Source0 (非基于 Port 的源)
- 操作:执行
performSelector:onThread:等由应用层触发的事件。 - 关键点:如果处理完 Source0 后,发现有 Source1 (基于 Port 的源) 已经准备好了,或者有主队列的 GCD 任务,可能会直接跳过休眠,去处理消息。
- 操作:执行
检查 Source1 (基于 Port 的源)
- 操作:如果有 Source1 处于 Ready 状态,直接跳转到 第9步 (处理消息)。
- 注:这是为了性能优化,如果有事做就不必休眠。
第二阶段:休眠 (Sleep)
通知 Observers:即将进入休眠
- 状态:
kCFRunLoopBeforeWaiting - 说明:这是 RunLoop 即将挂起、释放 CPU 资源的关键时刻。
- 状态:
休眠 (Wait for Mach Port)
- 操作:调用
mach_msg_trap,从用户态切换到内核态。 - 说明:线程停止运行,等待被唤醒。
- 唤醒条件(满足其一即可):
- 基于 Port 的 Source1 事件到达(如屏幕点击、网络数据)。
- Timer 定时器时间到了。
- GCD 主队列(Main Dispatch Queue)有任务分发。
- RunLoop 设置的超时时间到了。
- 操作:调用
第三阶段:唤醒与处理 (Wake Up & Handle)
通知 Observers:刚从休眠中唤醒
- 状态:
kCFRunLoopAfterWaiting - 说明:线程被唤醒,重新开始工作。
- 状态:
处理唤醒事件 (核心处理逻辑)
- 根据被唤醒的原因,执行相应的任务:
- 如果是 Timer 唤醒:触发 Timer 的回调。
- 如果是 GCD 唤醒:执行
dispatch_async(dispatch_get_main_queue(), block)中的 block。 - 如果是 Source1 唤醒:处理系统内核发来的事件(如触摸、系统通知)。
- 根据被唤醒的原因,执行相应的任务:
第四阶段:退出或重试 (Exit or Loop)
- 决定下一步
- 检查是否需要退出 Loop:
- 如果是
kCFRunLoopRunFinished。 - 如果指定了超时时间且已超时。
- 如果外部强制停止。
- 如果是
- 如果未退出:跳转回 第2步,开始新的一轮循环。
- 如果退出:通知 Observers
kCFRunLoopExit。
- 检查是否需要退出 Loop:
总结版(六步记忆法)
如果你为了面试或快速记忆,可以将上述繁琐步骤浓缩为以下“六步”逻辑:
- Notify Entry:通知进入 RunLoop。
- Process Pre-Sleep:通知处理 Timer/Source0,并执行 Source0。如果有 Source1 待处理,跳过休眠。
- Notify Sleep:通知即将休眠。
- Sleep:调用
mach_msg等待唤醒(Trap into Kernel)。 - Notify Wake:通知被唤醒。
- Handle Event:处理唤醒源(Timer / GCD / Source1),然后决定是 Loop 还是 Exit。
关键概念补充
- Source0:非基于 Port 的,用于应用层事件(如
performSelector)。需要手动标记为待处理并唤醒 RunLoop。 - Source1:基于 Mach Port 的,用于系统内核事件(如触摸、硬件中断)。能主动唤醒 RunLoop。
- GCD 与 RunLoop:只有 Main Dispatch Queue 的任务是由 RunLoop 处理的,其他队列由 GCD 线程池直接处理。
- 用户态/内核态切换:RunLoop 的核心价值在于 Step 7,利用
mach_msg让线程在没有消息时真正“停下来”不占用 CPU,有消息时又能毫秒级响应。