RunLoop 的循环机制本质上是一个受操作系统内核(Mach)驱动的 Event Loop。它通过一套精密的状态机,确保线程在有事做时全速运行,在无事做时进入真正的“深度睡眠”,从而平衡响应速度与能耗。
我们可以将 RunLoop 的一次完整迭代拆解为以下核心阶段:
1. RunLoop 循环逻辑全流程
当 RunLoop 启动后,它会进入一个 do-while 循环。
阶段一:准备与通知 (Entry)
- 通知 Observer:即将进入 Loop (
kCFRunLoopEntry)。 - 通知 Observer:即将处理 Timer (
kCFRunLoopBeforeTimers)。 - 通知 Observer:即将处理 Source (
kCFRunLoopBeforeSources)。
阶段二:处理非延迟事件 (Processing)
- 处理 Source0:执行这些非内核事件(如点击、手势处理、
performSelector)。 - 检查 Source1:如果有 Source1 待处理,跳过睡眠,直接跳转到第 9 步。
阶段三:休眠准备 (Waiting)
- 通知 Observer:即将进入休眠 (
kCFRunLoopBeforeWaiting)。这是处理 UI 刷新(Core Animation)和 AutoreleasePool 销毁重构的关键点。 - 内核态休眠:调用
mach_msg函数等待消息。此时线程挂起,不再消耗 CPU 指令。
阶段四:唤醒与处理 (Waking)
-
线程唤醒:当出现以下情况之一时,内核会唤醒线程:
- 收到基于 Mach Port 的 Source1 事件。
- Timer 时间到了。
- RunLoop 自身的超时时间到了。
- 被外部手动唤醒(
CFRunLoopWakeUp)。
-
通知 Observer:刚从休眠中唤醒 (
kCFRunLoopAfterWaiting)。 -
执行具体任务:根据唤醒原因,处理对应的 Timer 或 Source1。
阶段五:收尾与决策 (Exit/Loop)
- 通知 Observer:即将退出 Loop (
kCFRunLoopExit)。 - 决定去向:检查是否被停止、是否超时、或者是否还有 Mode Item。如果一切正常,回到第 2 步开始下一次迭代。
2. 深度睡眠的原理:Mach Port
RunLoop 之所以高效,是因为它不像普通 while 循环那样通过 sleep(1) 这种轮询方式,而是利用了 Mach Message。
- 用户态 内核态:当 RunLoop 调用
mach_msg时,它会让出 CPU 使用权。 - 内核监听:由操作系统内核负责监听对应的端口(Port)。
- 精准唤醒:只有当特定的电信号或消息到达端口时,内核才会恢复线程的上下文,让线程从之前停下的地方继续跑。
3. 对比:主线程 vs. 子线程
| 特性 | 主线程 RunLoop | 子线程 RunLoop |
|---|---|---|
| 创建时机 | 应用启动时由系统自动创建并运行。 | 只有在调用 [NSRunLoop currentRunLoop] 时才懒加载创建。 |
| 运行状态 | 默认始终处于 run 状态。 | 默认不运行,需要手动调用 run 方法。 |
| 退出机制 | 伴随 App 整个生命周期。 | 如果 Mode 中没有 Source 或 Timer,会立即退出。 |
4. 为什么子线程的 Timer 经常不跑?
这是新手常遇到的问题。原因就在于 RunLoop 的生命周期管理。
-
现象:在子线程
NSTimer.scheduledTimer(...),回调不触发。 -
根源:子线程的 RunLoop 虽然被创建了,但没有开启循环。代码执行完
scheduledTimer后线程就结束销毁了。 -
解决:必须手动运行 RunLoop:
Swift
autoreleasepool { let timer = Timer.scheduledTimer(...) RunLoop.current.run() // 开启循环,保持线程活跃 }
💡 一个有趣的细节
主线程的 RunLoop 实际上是由 UIApplicationMain 内部开启的。它就像一个巨大的漏斗,把零散的系统事件(重力感应、屏幕触摸、网络状态变化)汇聚成有序的任务序列,然后一个接一个地分发给你的代码。