iOS RunLoop 底层运行原理完全解析
引言
RunLoop(运行循环)是 iOS 线程并发模型的核心组件,是线程休眠、事件分发、定时器调度以及 UI 渲染等机制的基石。深刻理解 RunLoop 的工作原理,是写出高性能应用、进行卡顿监控与优化的必经之路。
本文将从数据结构、状态流转、Source 分类、事件协作、使用场景等多维度,对 RunLoop 进行严谨且全面的剖析。
一、RunLoop 的本质及其与线程的关系
1.1 什么是 RunLoop?
RunLoop 本质上是线程内部的一个 do-while 循环,负责“有事做事,无事休眠”。其核心由 CoreFoundation 框架实现,CFRunLoopRun() 会开启一个永不退出的循环(除非超时或手动停止),不断监听并响应各种事件。
关键特性:RunLoop 不是简单的忙等,而是通过 Mach 内核的
mach_msg()系统调用,使线程在内核层真正休眠,期间 CPU 占用为零。
1.2 线程与 RunLoop 的一一对应
- 每个线程都有且仅有一个与之关联的 RunLoop,由系统首次获取时自动创建。
- 主线程的 RunLoop 由系统自动启动(
UIApplicationMain内),开发者无需干预。 - 子线程的 RunLoop 必须由开发者手动获取
[NSRunLoop currentRunLoop]并调用run方法开启。 - 线程销毁时,其对应的 RunLoop 也会被销毁。
二、RunLoop 的核心数据结构
RunLoop 由五大核心组件构成,它们均由 CoreFoundation 框架定义:
2.1 CFRunLoopRef
代表一个 RunLoop 对象,每个线程唯一对应,内部维护了多个 Mode。
2.2 CFRunLoopModeRef
代表运行模式。RunLoop 在任何时刻只能运行在某一个 Mode 下,称为当前 Mode。Mode 内包含了该模式下需要监听的 Source0、Source1、Timer 和 Observer。
系统预置了以下常用 Mode:
| Mode | 含义 |
|---|---|
NSDefaultRunLoopMode |
应用空闲、无滚动时的默认状态 |
UITrackingRunLoopMode |
用户正在拖拽/滑动 ScrollView |
NSRunLoopCommonModes |
并非真实的 Mode,而是一个 Mode 集合的标签,默认包含上述两个 |
利用 Mode 切换可实现事件的隔离调度。
2.3 CFRunLoopSourceRef(事件源)
事件源分为两类:Source0 与 Source1。
Source0(非基于端口,被动型)
- 只包含一个函数指针,不具备 Mach 端口。
- 不能主动唤醒 RunLoop。
- 需要开发者手动调用
CFRunLoopSourceSignal将其标记为待处理,再调用CFRunLoopWakeUp唤醒 RunLoop。 - 典型应用:UIEvent(触摸、手势)、
performSelector:onThread:。
Source1(基于端口,主动型)
- 包含一个
mach_port和一个函数指针。 - 可主动唤醒 RunLoop:由内核或其他线程向该端口发送消息,内核自动唤醒 RunLoop 并执行回调。
- 典型应用:系统级事件(IOHIDEvent 捕获)、GCD 主队列任务、
NSMachPort线程通信。
2.4 CFRunLoopTimerRef
基于时间的触发器,底层对应于 NSTimer。Timer 必须加入到某个 Mode 中,满足时间条件后会唤醒 RunLoop 执行回调。
2.5 CFRunLoopObserverRef
观察者,用于监听 RunLoop 的六个关键状态。通过在特定环节注入回调,实现空闲任务调度、卡顿监控等功能。
三、RunLoop 完整状态流转详解
RunLoop 的一个完整生命周期会经历六个状态(CFRunLoopActivity 枚举)。以下是流转流程图及逐步骤解释:
逐步骤说明
Entry
CFRunLoopRun被调用,RunLoop 周期开始。BeforeTimers
通知观察者,即将检查所有 Timer 是否有需要触发的回调。BeforeSources
通知观察者,即将检查所有 Source0 是否有待处理事件。处理 Block
执行通过dispatch_async(dispatch_get_main_queue(), block)添加到主线程 GCD 队列的 Block。处理 Source0
处理已手动标记为待处理的 Source0 事件(如 UI 触摸事件)。决策点:检测 Source1
若此时已有 Source1 事件存在,则 goto 直接跳转到“处理 Source1”步骤,而不进入休眠。若无,则走正常休眠流程。BeforeWaiting
一切事件处理完毕,RunLoop 即将休眠。这是执行低优先级任务的绝佳时机(空闲调度)。线程休眠
调用mach_msg(),线程陷入内核态,CPU 停止运行此线程,直到有新消息到达对应端口。AfterWaiting
线程被唤醒后立即通知观察者。卡顿监控常利用此状态到下一次BeforeTimers之间的耗时来判断是否卡顿。处理唤醒源
根据唤醒来源,分三类处理,处理完毕后回到 BeforeTimers 重新开始下一轮循环:- Timer 到期 → 执行所有到期的 Timer 回调。
- GCD 主队列 → 执行主队列中等待的所有 Block。
- Source1 回调 → 直接执行该 Source1 的回调函数。
Exit
当 RunLoop 超时或外界调用CFRunLoopStop时触发,RunLoop 结束。
四、Source0 与 Source1 的深层解析
两者本质区别在于:是否拥有 Mach 端口,能否主动唤醒休眠的线程。
4.1 Source0
- 结构:仅包含一个
order(优先级)和一个callout函数指针。 - 唤醒机制:无法主动唤醒。需手动
CFRunLoopSourceSignal标记 +CFRunLoopWakeUp唤醒。 - 具体实例:
- UIEvent(触摸/手势):硬件事件到达后,由 Source1 的回调生成对应的 Source0,再由 RunLoop 在主线程分发。
- performSelector:
performSelector:onThread:系列方法本质是向目标线程添加一个 Source0。 - CFSocket:在 RunLoop 中表现为 Source0。
4.2 Source1
- 结构:包含一个
mach_port和一个callout函数指针。 - 唤醒机制:由内核自动唤醒,无需人工干预。
- 具体实例:
- 硬件事件监听:如
__IOHIDEventSystemClientQueueCallback,负责捕获触摸、加速计等硬件事件。 - GCD 主队列:主队列的 block 实际上是一个 Source1 事件。
- NSMachPort / NSMessagePort:线程/进程间通信。
- 硬件事件监听:如
4.3 触摸事件的完整流程(Source1 → Source0 协作)
- 手指触摸屏幕 → 底层驱动产生 IOHIDEvent。
- 系统 Source1 收到该硬件端口消息 → 唤醒主线程 RunLoop。
- Source1 的回调内部将 IOHIDEvent 封装成 UIEvent,再标记一个 Source0 为待处理。
- RunLoop 处理 Source0,分发事件到对应的 UIResponder 链。
五、使用场景与最佳实践
5.1 线程保活
子线程任务完成后会销毁,如需复用同一线程处理多次回调,可通过向该线程的 RunLoop 添加一个 Source 或 Timer 并调用 run,线程便会保持活跃。
- 退出时必须调用
CFRunLoopStop或移除所有输入源,否则线程泄漏。
5.2 定时器管理
NSTimer必须加入 RunLoop 的某个 Mode 中。- 加入
NSDefaultRunLoopMode时,滑动会暂停;加入NSRunLoopCommonModes则不受滑动影响。 CADisplayLink同理,可设置preferredFramesPerSecond降频。
5.3 滑动性能优化(Mode 隔离)
将耗时任务限制在 NSDefaultRunLoopMode 下执行,当用户滑动时系统自动切换到 UITrackingRunLoopMode,任务被忽略,手感流畅。
1 | [self performSelector:@selector(heavyTask) |
5.4 空闲时段任务调度
监听 kCFRunLoopBeforeWaiting 状态,在 RunLoop 即将休眠前执行分批低优先级任务(每次耗时 ≤ 10ms),如预解码图片、文本预排版等。
5.5 卡顿监控
监控两个耗时区间:
BeforeSources到BeforeWaitingAfterWaiting到BeforeTimers
若任一区间耗时超过阈值(如 50ms),即可判定卡顿,抓取堆栈定位问题。
5.6 AutoreleasePool 管理
主线程 RunLoop 内注册了两个 Observer:
- Entry 时创建 AutoreleasePool。
- BeforeWaiting 时释放旧池、创建新池,Exit 时最终释放。
六、性能优化注意事项
- 保证主线程轻量:耗时任务严禁在主线程 RunLoop 单次循环内。
- 减少无效唤醒:批量处理事件,跨线程通知优先用 Source1。
- 合并 Timer:避免创建大量独立 Timer,非 UI 定时器可用 GCD
dispatch_source代替。 - 空闲任务要分批:每次执行控制在 5~10ms,随后交还主线程。
- 线程生命周期:保活线程务必使用
@autoreleasepool包裹循环体,防止内存堆积。
七、常见误区
- 子线程默认没有开启 RunLoop,必须显式获取并运行。
NSRunLoopCommonModes不是真正的 Mode,只是组合标签。CFRunLoopStop仅停止一次循环,若线程再次调用run,RunLoop 会重新启动。- 在 Observer 回调中阻塞 RunLoop 或形成循环引用,将导致线程无法正常退出。
八、结语
RunLoop 的底层原理是 iOS 开发的分水岭。从线程休眠到事件驱动,从滑动优化到帧率监控,它无处不在地影响着应用的性能和体验。掌握 RunLoop 的本质,意味着你能够游刃有余地驾驭线程模型,写出更快、更流畅的 App。
注:文中部分代码与流程参考自 Apple CoreFoundation 开源代码及 CFRunLoop 内部调用栈。