将来的自己,会感谢现在努力的自己!

0%

iOS RunLoop (一) 底层运行原理完全解析

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 枚举)。以下是流转流程图及逐步骤解释:

逐步骤说明

  1. Entry
    CFRunLoopRun 被调用,RunLoop 周期开始。

  2. BeforeTimers
    通知观察者,即将检查所有 Timer 是否有需要触发的回调。

  3. BeforeSources
    通知观察者,即将检查所有 Source0 是否有待处理事件。

  4. 处理 Block
    执行通过 dispatch_async(dispatch_get_main_queue(), block) 添加到主线程 GCD 队列的 Block。

  5. 处理 Source0
    处理已手动标记为待处理的 Source0 事件(如 UI 触摸事件)。

  6. 决策点:检测 Source1
    若此时已有 Source1 事件存在,则 goto 直接跳转到“处理 Source1”步骤,而不进入休眠。若无,则走正常休眠流程。

  7. BeforeWaiting
    一切事件处理完毕,RunLoop 即将休眠。这是执行低优先级任务的绝佳时机(空闲调度)。

  8. 线程休眠
    调用 mach_msg(),线程陷入内核态,CPU 停止运行此线程,直到有新消息到达对应端口。

  9. AfterWaiting
    线程被唤醒后立即通知观察者。卡顿监控常利用此状态到下一次 BeforeTimers 之间的耗时来判断是否卡顿。

  10. 处理唤醒源
    根据唤醒来源,分三类处理,处理完毕后回到 BeforeTimers 重新开始下一轮循环

    • Timer 到期 → 执行所有到期的 Timer 回调。
    • GCD 主队列 → 执行主队列中等待的所有 Block。
    • Source1 回调 → 直接执行该 Source1 的回调函数。
  11. Exit
    当 RunLoop 超时或外界调用 CFRunLoopStop 时触发,RunLoop 结束。


四、Source0 与 Source1 的深层解析

两者本质区别在于:是否拥有 Mach 端口,能否主动唤醒休眠的线程

4.1 Source0

  • 结构:仅包含一个 order(优先级)和一个 callout 函数指针。
  • 唤醒机制:无法主动唤醒。需手动 CFRunLoopSourceSignal 标记 + CFRunLoopWakeUp 唤醒。
  • 具体实例
    • UIEvent(触摸/手势):硬件事件到达后,由 Source1 的回调生成对应的 Source0,再由 RunLoop 在主线程分发。
    • performSelectorperformSelector:onThread: 系列方法本质是向目标线程添加一个 Source0。
    • CFSocket:在 RunLoop 中表现为 Source0。

4.2 Source1

  • 结构:包含一个 mach_port 和一个 callout 函数指针。
  • 唤醒机制:由内核自动唤醒,无需人工干预。
  • 具体实例
    • 硬件事件监听:如 __IOHIDEventSystemClientQueueCallback,负责捕获触摸、加速计等硬件事件。
    • GCD 主队列:主队列的 block 实际上是一个 Source1 事件。
    • NSMachPort / NSMessagePort:线程/进程间通信。

4.3 触摸事件的完整流程(Source1 → Source0 协作)

  1. 手指触摸屏幕 → 底层驱动产生 IOHIDEvent。
  2. 系统 Source1 收到该硬件端口消息 → 唤醒主线程 RunLoop。
  3. Source1 的回调内部将 IOHIDEvent 封装成 UIEvent,再标记一个 Source0 为待处理。
  4. 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
2
3
4
[self performSelector:@selector(heavyTask) 
withObject:nil
afterDelay:0
inModes:@[NSDefaultRunLoopMode]];

5.4 空闲时段任务调度

监听 kCFRunLoopBeforeWaiting 状态,在 RunLoop 即将休眠前执行分批低优先级任务(每次耗时 ≤ 10ms),如预解码图片、文本预排版等。

5.5 卡顿监控

监控两个耗时区间:

  • BeforeSourcesBeforeWaiting
  • AfterWaitingBeforeTimers
    若任一区间耗时超过阈值(如 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 内部调用栈。