7-5.【OC】【RunLoop】RunLoop 的循环机制是怎样的?

4 阅读3分钟

RunLoop 的循环机制本质上是一个受操作系统内核(Mach)驱动的 Event Loop。它通过一套精密的状态机,确保线程在有事做时全速运行,在无事做时进入真正的“深度睡眠”,从而平衡响应速度与能耗。

我们可以将 RunLoop 的一次完整迭代拆解为以下核心阶段:


1. RunLoop 循环逻辑全流程

当 RunLoop 启动后,它会进入一个 do-while 循环。

阶段一:准备与通知 (Entry)

  1. 通知 Observer:即将进入 Loop (kCFRunLoopEntry)。
  2. 通知 Observer:即将处理 Timer (kCFRunLoopBeforeTimers)。
  3. 通知 Observer:即将处理 Source (kCFRunLoopBeforeSources)。

阶段二:处理非延迟事件 (Processing)

  1. 处理 Source0:执行这些非内核事件(如点击、手势处理、performSelector)。
  2. 检查 Source1:如果有 Source1 待处理,跳过睡眠,直接跳转到第 9 步。

阶段三:休眠准备 (Waiting)

  1. 通知 Observer:即将进入休眠 (kCFRunLoopBeforeWaiting)。这是处理 UI 刷新(Core Animation)和 AutoreleasePool 销毁重构的关键点。
  2. 内核态休眠:调用 mach_msg 函数等待消息。此时线程挂起,不再消耗 CPU 指令。

阶段四:唤醒与处理 (Waking)

  1. 线程唤醒:当出现以下情况之一时,内核会唤醒线程:

    • 收到基于 Mach Port 的 Source1 事件。
    • Timer 时间到了。
    • RunLoop 自身的超时时间到了。
    • 被外部手动唤醒(CFRunLoopWakeUp)。
  2. 通知 Observer:刚从休眠中唤醒 (kCFRunLoopAfterWaiting)。

  3. 执行具体任务:根据唤醒原因,处理对应的 Timer 或 Source1。

阶段五:收尾与决策 (Exit/Loop)

  1. 通知 Observer:即将退出 Loop (kCFRunLoopExit)。
  2. 决定去向:检查是否被停止、是否超时、或者是否还有 Mode Item。如果一切正常,回到第 2 步开始下一次迭代。

2. 深度睡眠的原理:Mach Port

RunLoop 之所以高效,是因为它不像普通 while 循环那样通过 sleep(1) 这种轮询方式,而是利用了 Mach Message

  • 用户态 \rightarrow 内核态:当 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 内部开启的。它就像一个巨大的漏斗,把零散的系统事件(重力感应、屏幕触摸、网络状态变化)汇聚成有序的任务序列,然后一个接一个地分发给你的代码。