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 滑动为例:
-
Default 状态:主线程 RunLoop 在
kCFRunLoopDefaultMode中循环。 -
触发触摸:系统内核通过 Mach Port 发送 Source1 消息。
-
模式切换:
- RunLoop 收到切换请求。
- 通知 Observer:即将退出当前 Mode (
kCFRunLoopExit)。 - 切换指针:
_currentMode变为UITrackingRunLoopMode。 - 通知 Observer:即将进入新 Mode (
kCFRunLoopEntry)。
-
重新迭代: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 通知) | 同左 |