7-8.【OC】【RunLoop】CFRunLoopMode / NSRunLoopMode 的 Mode 切换是如何影响事件调度的?

3 阅读3分钟

RunLoop 的 Mode 切换本质上是一场事件源的“物理隔离” 。它通过在运行时动态切换数据集合,强制让线程只关注当前模式下的任务。

我们可以从底层数据结构和调度逻辑两个维度来剖析这一过程:


1. 底层数据结构:多集合并存

CFRunLoop.c 的源码中,一个 CFRunLoop 结构体并不直接包含事件源,而是包含一个名为 _modes 的集合(Set)。

  • Mode 的独立性:每个 CFRunLoopMode 内部都维护着自己独立的 _sources0_sources1_timers_observers
  • 切换的本质:所谓的“Mode 切换”,就是 RunLoop 在执行 do-while 循环的下一次迭代前,将一个名为 _currentMode 的指针从 Mode A 指向 Mode B。

2. 切换过程:先退出,再进入

Mode 的切换并不是平滑过渡的,而是伴随着循环的重新启动

UITableView 滑动为例:

  1. Default 状态:主线程 RunLoop 在 kCFRunLoopDefaultMode 中循环。

  2. 触发触摸:系统内核通过 Mach Port 发送 Source1 消息。

  3. 模式切换

    • RunLoop 收到切换请求。
    • 通知 Observer:即将退出当前 Mode (kCFRunLoopExit)。
    • 切换指针_currentMode 变为 UITrackingRunLoopMode
    • 通知 Observer:即将进入新 Mode (kCFRunLoopEntry)。
  4. 重新迭代:RunLoop 重新开始执行循环逻辑,但此时它只去查 UITrackingRunLoopMode 下的任务列表。


3. 对事件调度的具体影响

这种机制导致了以下三种典型的调度行为:

A. 任务挂起(The Pause)

如果一个任务(如 NSURLConnection 的回调或 NSTimer)注册在 Default Mode,而当前处于 Tracking Mode,那么:

  • 该任务的事件即便到达了(信号已发),RunLoop 在扫描当前 Mode 的任务列表时也会直接跳过它。
  • 任务不会丢失,而是处于“待处理”状态,直到 Mode 切回 Default。

B. Common Modes 的“同步同步”

由于 CommonModes 实际上是一个标签(Item 标记),当你把一个 Timer 加入 Common 时:

  • 系统会将这个 Timer 同步拷贝到所有具有 "Common" 属性的真实 Mode(Default 和 Tracking)中。
  • 影响:无论 Mode 怎么切,对应的任务列表里都有这个 Timer,从而实现了跨模式的调度连续性。

C. 优先级保活(Priority Preservation)

由于 UITrackingRunLoopMode 下的任务通常很少(只负责处理触摸和简单的 UI 刷新),Mode 切换有效地清空了那些杂乱的后台 Source0 任务,使得主线程能以接近 100% 的精力处理每秒 60/120 帧的渲染。


4. 真实案例:CADisplayLink 与滑动

CADisplayLink 是一个特殊的 Timer,它与屏幕刷新率同步。

  • 意外行为:如果你直接把它加到 Default Mode,滑动列表时动画会立刻卡死(停止)。
  • 调度真相:此时 GPU 仍在刷新,但因为 RunLoop 切换到了 Tracking Mode,它不再调用 CADisplayLink 的回调方法,导致下一帧的动画数据无法提交。

总结:调度逻辑对照

维度Mode 切换前 (Default)Mode 切换后 (Tracking)
可见任务所有 App 常规逻辑、普通 Timer仅 UI 追踪相关任务、Common 任务
CPU 分配分散处理网络、定时器、逻辑运算高度集中在 Source1 (Touch) 和渲染
调度延迟较低(取决于任务量)极低(保证 16ms 内响应)
切换开销极小(仅指针操作和 Observer 通知)同左