RunLoop 完全知识体系(最终版)
一、RunLoop 思维导图(完整版)
# RunLoop 知识体系
## 1. 定义与核心概念
- **什么是 RunLoop**
- 运行循环 / 事件循环(Event Loop)
- 本质:一个 do-while 循环,持续运行,处理事件,无事休眠
- **为什么需要 RunLoop**
- 保持程序持续运行(主线程不会退出)
- 处理各种事件:触摸、定时器、网络回调、界面刷新、PerformSelector
- 节省 CPU 资源:有事件时处理,无事件时休眠(mach_msg 机制)
- 实现自动释放池的自动释放(每次循环结束前释放)
- **RunLoop 的基本特性**
- 事件驱动(Event-driven)
- 支持嵌套运行(通过 Mode 切换)
- 与线程一对一绑定
## 2. RunLoop 与线程的关系
- **一对一映射**
- 每条线程有唯一对应的 RunLoop 对象
- 保存在全局字典中(线程为 key,RunLoop 为 value)
- **生命周期**
- 主线程 RunLoop:程序启动时自动创建并运行
- 子线程 RunLoop:第一次获取时创建(`[NSRunLoop currentRunLoop]`),需要手动调用 `run` 才能启动
- 线程结束时,RunLoop 自动销毁
- **获取方式**
- Foundation:`[NSRunLoop currentRunLoop]`(当前线程)、`[NSRunLoop mainRunLoop]`(主线程)
- CoreFoundation:`CFRunLoopGetCurrent()`、`CFRunLoopGetMain()`
## 3. RunLoop 核心类(CoreFoundation)
- **CFRunLoopRef**
- 代表 RunLoop 对象
- 包含多个 Mode,当前 Mode 等
- **CFRunLoopModeRef**
- 运行模式,隔离事件源
- 结构包含:Source0 数组、Source1 数组、Timer 数组、Observer 数组
- **CFRunLoopSourceRef**(事件源)
- **Source0**:非基于端口,需手动唤醒(触摸事件、performSelector、事件分发)
- **Source1**:基于 mach_port 的通信,能主动唤醒 RunLoop(系统事件、其他线程消息)
- **CFRunLoopTimerRef**(定时器)
- 基于时间的触发器,NSTimer 的底层
- 包含触发时间、间隔、是否重复等
- **CFRunLoopObserverRef**(观察者)
- 监听 RunLoop 状态变化(6 种状态)
- 用于自动释放池管理、卡顿监控等
## 4. RunLoop 的 Mode(运行模式)
- **Mode 的概念**
- 每个 Mode 是一组 Source/Timer/Observer 的集合
- RunLoop 每次只能运行在一个 Mode 下,称为 CurrentMode
- 切换 Mode 必须退出当前 Loop,再重新进入
- **常见 Mode**
- **NSDefaultRunLoopMode**(默认模式)
- App 空闲时运行,处理除滑动外的大部分事件
- 主线程默认在此模式下运行
- **UITrackingRunLoopMode**(界面跟踪模式)
- ScrollView 滑动时自动切换到此模式,保证滑动流畅
- **NSRunLoopCommonModes**(通用模式集合)
- 不是一个具体 Mode,而是一个标记(一组 Mode 的集合)
- 向 CommonModes 添加 Source/Timer,等同于添加到集合内的所有 Mode(通常包括 Default 和 Tracking)
- **其他系统 Mode**(了解)
- `UIInitializationRunLoopMode`:应用启动时使用
- `GSEventReceiveRunLoopMode`:图形事件接收模式
- **Mode 的作用**
- 隔离不同场景事件,互不干扰
- 提高性能:只监听当前 Mode 需要的事件,减少无效唤醒
- 实现优先级:滑动时优先处理 Tracking 事件,默认任务延迟
## 5. RunLoop 运行逻辑(11 步详解)
1. **进入 Loop**:通知 Observers(`kCFRunLoopEntry`)
2. **即将处理 Timers**:通知 Observers(`kCFRunLoopBeforeTimers`)
3. **即将处理 Sources**:通知 Observers(`kCFRunLoopBeforeSources`)
4. **处理 Blocks**(非延迟的 block,如 `CFRunLoopPerformBlock`)
5. **处理 Source0**(用户事件,可能会再次触发 Blocks)
6. **如果有 Source1**,跳转到步骤 8(立即处理端口事件)
7. **即将休眠**:通知 Observers(`kCFRunLoopBeforeWaiting`)
- 调用 `mach_msg` 进入内核态等待消息
8. **结束休眠**:被消息唤醒,通知 Observers(`kCFRunLoopAfterWaiting`)
- 处理唤醒原因:
- **Timer 到期**(通过 `__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__`)
- **GCD 主队列任务**(`__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__`)
- **Source1 事件**(`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__`)
9. **处理 Blocks**
10. **根据结果决定继续或退出**
- 若还有事件未处理 → 回到步骤 2
- 若超时或收到停止指令 → 退出 Loop
11. **退出 Loop**:通知 Observers(`kCFRunLoopExit`)
## 6. RunLoop 底层实现
- **mach_msg 机制**
- XNU 内核的 IPC 机制,用于线程间通信和休眠唤醒
- 调用 `mach_msg` 时,线程进入内核态,等待消息;有消息到达时,内核唤醒线程
- **RunLoop 对象结构(源码)**
- `__CFRunLoop`:包含 pthread、currentMode、modes 集合、commonModes 等
- `__CFRunLoopMode`:包含 name、sources0、sources1、observers、timers 等
- `__CFRunLoopSource`:包含优先级、版本、回调函数等
- **RunLoop 与 GCD**
- GCD 主队列任务通过 `__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__` 在 RunLoop 中执行
- RunLoop 与 GCD 的 `dispatch_main_queue` 紧密集成
- **RunLoop 与 AutoreleasePool**
- 每个 RunLoop 循环开始前创建一个自动释放池,循环结束时(BeforeWaiting 或 Exit)释放
- 通过 Observer 监听 `kCFRunLoopBeforeWaiting` 实现
## 7. RunLoop 的状态(Observer 监听)
- **`kCFRunLoopEntry`**:即将进入 Loop
- **`kCFRunLoopBeforeTimers`**:即将处理 Timer
- **`kCFRunLoopBeforeSources`**:即将处理 Source
- **`kCFRunLoopBeforeWaiting`**:即将休眠
- **`kCFRunLoopAfterWaiting`**:刚从休眠唤醒
- **`kCFRunLoopExit`**:即将退出 Loop
## 8. RunLoop 的实际应用
- **线程保活(常驻线程)**
- 在子线程中添加 Source 或 Timer,并运行 RunLoop,防止线程退出
- 应用:AFNetworking 网络请求回调线程、后台监控任务
- 实现方式:`[[NSRunLoop currentRunLoop] run]` + 添加一个永远不触发的 Source
- **解决 NSTimer 滑动失效**
- 原因:滑动时 RunLoop 切换到 TrackingMode,DefaultMode 的 Timer 被暂停
- 解决方案:
1. 将 Timer 添加到 `NSRunLoopCommonModes`
2. 在子线程中创建 Timer(独立 RunLoop)
3. 使用 GCD 定时器(`dispatch_source_t`)不依赖 RunLoop
- **监控 App 卡顿**
- 原理:监听 RunLoop 状态,计算 BeforeWaiting 和 AfterWaiting 的时间差
- 若耗时超过阈值(如 50ms),判定为卡顿,记录堆栈
- 利用 Observer 注册 `kCFRunLoopBeforeSources` 和 `kCFRunLoopBeforeWaiting`
- **性能优化**
- 控制自动释放池释放时机:避免在循环中创建大量临时对象导致内存峰值
- 合理使用 Mode:将不需要在滑动时执行的任务放在 DefaultMode,减少 TrackingMode 的负担
- 优化事件响应:减少 Source0 处理时间,避免阻塞 RunLoop
- **手势识别 & UI 刷新**
- 触摸事件通过 Source1 唤醒 RunLoop,Source0 处理事件分发
- 界面刷新(`setNeedsLayout`、`setNeedsDisplay`)在 RunLoop 即将休眠前通过 Blocks 执行(CATransaction 提交)
- **PerformSelector 的实现**
- `performSelector:withObject:afterDelay:` 基于 Timer 实现(添加到 RunLoop)
- `performSelectorOnMainThread:` 等通过 Source0 或 GCD 实现
## 9. RunLoop 与系统框架的关系
- **UIKit**
- 事件响应、手势识别、界面刷新都依赖主线程 RunLoop
- UI 更新延迟到 RunLoop 结束前统一处理(CATransaction)
- **CoreAnimation**
- 动画提交在 RunLoop 的 BeforeWaiting 阶段进行(CATransaction 提交)
- 通过 Observer 监听实现动画同步
- **Foundation**
- NSRunLoop 是对 CFRunLoop 的 OC 封装
- NSTimer、NSURLConnection(已废弃)等基于 RunLoop
- **GCD**
- 主队列任务通过 RunLoop 唤醒执行
- 其他队列不依赖 RunLoop
## 10. RunLoop 的启动与退出
- **启动 RunLoop 的方式**
- `- (void)run;`:无限期运行,无法停止(不推荐)
- `- (void)runUntilDate:(NSDate *)limitDate;`:运行到指定时间,超时退出
- `- (void)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;`:在指定 Mode 运行一次循环,处理完事件或超时后退出
- `CFRunLoopRun()` / `CFRunLoopRunInMode()`:CoreFoundation 层启动
- **退出 RunLoop 的方式**
- 设置超时时间自动退出
- 调用 `CFRunLoopStop()` 停止
- 移除所有事件源,RunLoop 会退出
- 线程销毁时自动退出
## 11. RunLoop 的局限性
- **时间精度受限**:Timer 依赖于 RunLoop 循环,可能延迟(滑动时尤其明显)
- **模式切换开销**:切换 Mode 需要退出再进入,有性能损耗
- **不适合高精度实时任务**:推荐使用 GCD 的 Dispatch Source 或实时线程
- **内存管理风险**:常驻线程中的 RunLoop 需注意循环引用
## 12. 常见面试题速览
- RunLoop 是什么?作用?
- RunLoop 与线程的关系?
- Timer 与 RunLoop 的关系?滑动时失效如何解决?
- RunLoop 内部实现逻辑?(11 步流程)
- RunLoop 的 Mode 有哪些?作用?
- RunLoop 的状态有哪些?
- RunLoop 如何响应用户操作?
- RunLoop 的应用场景?(保活、卡顿监控、定时器优化等)
二、RunLoop 面试题及答案(20题)
1. 讲讲 RunLoop,项目中有用到吗?
专业答案(含级别标注)
【初级掌握】RunLoop 是一个事件处理循环,用于调度任务、处理输入事件,并让线程在没有任务时休眠以节省 CPU。每个线程都有唯一对应的 RunLoop,主线程的 RunLoop 在程序启动时自动运行。
【中级扩展】实际开发中常见应用包括:
- 线程保活:例如 AFNetworking 中创建常驻线程处理网络回调,避免线程频繁创建销毁。
- 定时器管理:NSTimer 必须加入 RunLoop 的某个 Mode 才能正常工作,否则不会触发。
- 滑动流畅性优化:将 UI 更新任务放在 NSDefaultRunLoopMode,滑动时切换到 UITrackingRunLoopMode,避免卡顿。
- 性能监控:通过监听 RunLoop 状态(BeforeWaiting 和 AfterWaiting)计算耗时,检测卡顿。
-
自动释放池优化:RunLoop 每次循环结束时释放自动释放池,减少内存峰值。
【高级深入】RunLoop 的底层基于mach_msg实现休眠唤醒,其数据结构包括__CFRunLoop、__CFRunLoopMode、__CFRunLoopSource等,源码可在 CF 开源库中查看。高级应用包括自定义 Mode、结合 Dispatch Source 实现高效事件处理,以及利用 RunLoop 特性设计精准的卡顿监控工具。
通俗解释
RunLoop 就像程序的心脏,不停跳动,有任务时处理,没任务时休息。主线程的 RunLoop 保证了 App 能一直响应你的操作。在项目中,我用它来让一个后台线程常驻(比如网络请求回调线程),避免频繁创建销毁线程(线程保活);也用它解决过滑动时定时器暂停的问题(将 Timer 加入 CommonModes);还用它监控 App 是否卡顿(通过监听 RunLoop 状态耗时)。
2. RunLoop 内部实现逻辑?
专业答案(含级别标注)
【初级掌握】RunLoop 内部是一个 do-while 循环,每次循环会检查事件并处理,没有事件时休眠。
【中级扩展】具体步骤如下:
- 通知观察者即将进入 RunLoop(
kCFRunLoopEntry)。 - 通知观察者即将处理 Timer 事件(
kCFRunLoopBeforeTimers)。 - 通知观察者即将处理 Source 事件(
kCFRunLoopBeforeSources)。 - 处理 Blocks(非延迟的 block)。
- 处理 Source0(触摸事件、performSelector 等用户触发的事件,可能会再次触发 Blocks)。
- 如果有 Source1(基于端口的通信),跳转到第 8 步立即处理;否则继续。
- 通知观察者线程即将休眠(
kCFRunLoopBeforeWaiting),并调用mach_msg进入内核态等待消息。 - 被唤醒后通知观察者(
kCFRunLoopAfterWaiting),然后处理唤醒原因:- 处理 Timer(定时器到期)
- 处理 GCD 异步主队列任务(
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__) - 处理 Source1(端口消息)
- 处理 Blocks。
- 根据结果决定继续循环或退出(如超时、停止指令等)。
- 通知观察者退出 Loop(
kCFRunLoopExit)。
【高级深入】RunLoop 通过mach_msg实现休眠,无事件时线程进入内核态休眠,有消息时被唤醒继续执行。深入理解需研究 CFRunLoop 源码,掌握其与 GCD、AutoreleasePool 的交互机制,以及如何利用 RunLoop 实现线程间通信和自定义事件源。
通俗解释
RunLoop 内部是一个 do-while 循环,每次循环都会检查是否有事件需要处理。如果有,就处理;如果没有,就通过 mach_msg 让线程进入休眠状态,等待系统唤醒。整个过程就像一个人:醒来干活(处理事件),干完活就睡觉(休眠),直到被闹钟(定时器)或门铃(端口消息)叫醒。
3. RunLoop 和线程的关系?
专业答案(含级别标注)
【初级掌握】每个线程有且仅有一个与之关联的 RunLoop 对象。主线程的 RunLoop 在程序启动时自动创建并运行;子线程默认没有 RunLoop,需要手动获取并开启。
【中级扩展】RunLoop 保存在全局字典中,线程为 key,RunLoop 为 value。RunLoop 在第一次获取时创建(调用 [NSRunLoop currentRunLoop]),在线程结束时销毁。子线程若需使用 RunLoop,必须调用 run 方法启动,否则线程执行完任务后会退出。
【高级深入】RunLoop 与线程的生命周期紧密绑定,其底层通过 pthread 线程局部存储实现。高级应用包括线程保活(如 AFNetworking 常驻线程),需在 RunLoop 中添加自定义 Source 或 Port 防止自动退出,并合理管理内存避免循环引用。
通俗解释
每个线程都有自己的 RunLoop,就像每个人都有自己的日程表。主线程的日程表(RunLoop)是自动开启的,保证 App 能一直响应你的操作。而子线程默认没有日程表,你需要主动说“我要开始循环工作了”才会启动。当线程被销毁时,它的日程表也会被销毁。
4. Timer 与 RunLoop 的关系?
专业答案(含级别标注)
【初级掌握】NSTimer 必须添加到 RunLoop 的某个 Mode 中才能被触发。scheduledTimerWith... 方法默认将 Timer 添加到当前线程 RunLoop 的 NSDefaultRunLoopMode。
【中级扩展】RunLoop 在每次循环中检查 Timer 是否到期,如果 Timer 所在的 Mode 当前未被 RunLoop 激活,则 Timer 不会被触发。例如,滑动 ScrollView 时 RunLoop 切换到 UITrackingRunLoopMode,此时 NSDefaultRunLoopMode 下的 Timer 不会执行,导致定时器“失效”。解决办法:将 Timer 添加到 NSRunLoopCommonModes(包含默认和追踪两种 Mode),或手动添加到追踪模式。
【高级深入】Timer 的底层是 CFRunLoopTimerRef,基于 mk_timer 或时钟事件实现。RunLoop 对 Timer 的管理涉及时间阀值、延迟容忍等。高级应用包括使用 dispatch_source_t 创建高精度定时器以规避 RunLoop 模式切换的影响,或利用 CFRunLoopTimerCreate 自定义定时器行为。
通俗解释
NSTimer 就像你设的一个闹钟,它必须放在某个“房间”(Mode)里才能响。默认情况下,闹钟放在“默认房间”,但当你滑动屏幕时,系统切换到了“追踪房间”,默认房间的闹钟就听不到了。解决办法是把闹钟放在“通用房间”,这样在任何房间都能听到。
5. 程序中添加每 3 秒响应一次的 NSTimer,当拖动 tableview 时 timer 可能无法响应要怎么解决?
专业答案(含级别标注)
【初级掌握】解决方法是将 Timer 添加到 CommonModes:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
CommonModes 是一个集合,默认包含 NSDefaultRunLoopMode 和 UITrackingRunLoopMode,这样 Timer 在两个 Mode 下都能工作。
【中级扩展】另外两种方案:
- 在子线程中创建 Timer:子线程 RunLoop 不受主线程滑动影响,但需注意线程保活和 UI 更新需切回主线程。
- 使用 CADisplayLink 或 GCD 定时器:CADisplayLink 也受 Mode 影响,同样可添加到 CommonModes;GCD 定时器基于队列,不依赖 RunLoop,但要注意循环引用和线程安全。
推荐方案:使用第一种,简单有效。
【高级深入】深入分析:NSTimer 依赖于 RunLoop 的时间点检查,而 RunLoop 在UITrackingRunLoopMode下不处理默认模式的 Timer,这是设计使然。高级解决方案包括自定义 Mode 并控制其切换,或者利用 CFRunLoop 的CFRunLoopAddTimer精细管理。对于高精度定时需求,可采用mach_absolute_time结合dispatch_source实现。
通俗解释
这是典型的“房间切换”问题。滑动时系统切换到“追踪房间”,而你的闹钟在“默认房间”,所以听不到。解决办法:
- 把闹钟放到“通用房间”(CommonModes),这样在默认和追踪房间都能听到。
- 把闹钟放到另一个房间(子线程),但要注意子线程需要开启 RunLoop 且 UI 更新必须切回主线程。
- 换一个不用房间的闹钟,比如 GCD 定时器,它不依赖 RunLoop。
6. RunLoop 是怎么响应用户操作的,具体流程是什么样的?
专业答案(含级别标注)
【初级掌握】用户操作通过 Source0 和 Source1 协同处理:Source1 负责唤醒 RunLoop,Source0 负责具体事件分发。
【中级扩展】详细流程:
- 用户触摸屏幕,硬件触发中断,系统生成
IOHIDEvent。 - 通过
mach_msg传递给应用进程的 Source1(基于端口的通信)。 - RunLoop 在休眠中被唤醒,处理 Source1,将事件交给
_UIApplicationHandleEventQueue。 - 系统将事件转换为
UIEvent,并通过 Source0 回调(__IOHIDEventSystemClientQueueCallback)分发到 UIWindow,最终找到合适的响应者处理。 - 事件处理过程中可能触发
setNeedsLayout等界面刷新标记,RunLoop 在即将休眠前会处理 Blocks,执行界面更新(如CATransaction提交)。
【高级深入】整个过程涉及 IOKit 框架、CoreAnimation 事务提交机制。高级开发者需理解mach_msg在内核与用户态之间的传递,以及 RunLoop 如何与dispatch队列交互。可结合 Instruments 的 RunLoop 工具分析事件响应延迟,优化交互性能。
通俗解释
用户触摸屏幕后,系统内核捕捉到硬件信号,通过 mach_msg 发送给 App 进程的 Source1(这是一个端口事件),唤醒 RunLoop。RunLoop 醒来后,Source1 把事件交给内部队列,然后 Source0 被触发,负责把事件分发到具体的视图和响应链上。整个过程就像:门铃响了(Source1唤醒),你去开门,然后把客人(事件)引导到正确的房间(视图)。
7. 说说 RunLoop 的几种状态
专业答案(含级别标注)
【初级掌握】CFRunLoopObserver 可观察 RunLoop 的活动状态,常见的有进入、处理 Timer、处理 Source、休眠、唤醒、退出等。
【中级扩展】具体 6 种状态:
-
kCFRunLoopEntry:即将进入 Loop -
kCFRunLoopBeforeTimers:即将处理 Timer -
kCFRunLoopBeforeSources:即将处理 Source -
kCFRunLoopBeforeWaiting:即将进入休眠 -
kCFRunLoopAfterWaiting:刚从休眠唤醒 -
kCFRunLoopExit:即将退出 Loop
开发者可通过添加 Observer 监听这些状态来执行特定任务,如监控卡顿(计算 BeforeWaiting 和 AfterWaiting 的耗时)。
【高级深入】Observer 的底层基于CFRunLoopObserverRef,其回调在 RunLoop 特定阶段触发。高级应用包括利用 Observer 实现精准的自动释放池管理、检测主线程卡顿时获取堆栈信息,以及结合 CADisplayLink 实现帧率监控。
通俗解释
RunLoop 在运行过程中会经历几个关键节点,就像人的一天:起床(Entry)、准备吃饭(BeforeTimers)、准备工作(BeforeSources)、准备睡觉(BeforeWaiting)、被叫醒(AfterWaiting)、睡觉(Exit)。我们可以通过 Observer 来监听这些节点,比如在准备睡觉时做点事情(如自动释放池释放)。
8. RunLoop 的 Mode 作用是什么?
专业答案(含级别标注)
【初级掌握】Mode 用于隔离不同组的事件源,使 RunLoop 可以在不同场景下选择性地处理事件,避免互相干扰。常见 Mode 有 NSDefaultRunLoopMode、UITrackingRunLoopMode。
【中级扩展】每个 Mode 包含一组 Source、Timer、Observer,RunLoop 启动时只能选择其中一个 Mode 运行。切换 Mode 必须退出当前 Loop,再重新进入另一个 Mode。作用包括:
-
实现优先级:滑动时只处理
UITrackingRunLoopMode中的事件,保证滑动流畅。 - 提升性能:减少无效唤醒,只在需要的 Mode 下监听事件。
-
CommonModes:不是一个具体 Mode,而是一组 Mode 的集合,向 CommonModes 添加 Source/Timer 意味着该 Source/Timer 会被加入集合内的所有 Mode(如默认和追踪)。
【高级深入】深入理解需研究 CFRunLoop 源码中 Mode 的管理机制,如__CFRunLoopFindMode、__CFRunLoopModeIsEmpty等。高级应用包括自定义 Mode 实现特定场景下的任务隔离,以及利用 Mode 切换优化 App 后台任务执行策略。
通俗解释
Mode 就像不同的工作场景:你在办公室(DefaultMode)处理邮件,在会议室(TrackingMode)专注演示。如果会议中突然有邮件提醒,你会先忽略(不处理 DefaultMode 的事件),等会议结束再处理。这样保证了当前场景的流畅性。CommonModes 则是一个“VIP 通行证”,让你的人(Timer)可以在多个场景中出现。
9. RunLoop 有哪些启动方式?有什么区别?
专业答案(含级别标注)
【初级掌握】RunLoop 有三种启动方式:run、runUntilDate:、runMode:beforeDate:。
【中级扩展】区别:
-
- (void)run:无限期运行,无法手动停止,除非 RunLoop 被移除所有事件源。不推荐使用,因为无法控制退出。 -
- (void)runUntilDate:(NSDate *)limitDate:运行到指定时间,超时后自动退出。可以用于定时执行任务。 -
- (void)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate:在指定 Mode 下运行一次循环,处理完一个事件或超时后退出。这是最可控的方式,常用于线程保活中循环调用。
【高级深入】底层对应 CFRunLoop 的CFRunLoopRun和CFRunLoopRunInMode。高级应用中,可以通过CFRunLoopStop强制停止 RunLoop。在实现常驻线程时,通常使用runMode:beforeDate:配合 while 循环,并设置一个永不过期的 Source 来保持 RunLoop 运行。
通俗解释
启动 RunLoop 就像开启一个工作模式:run 是“一直干到死”(不推荐),runUntilDate: 是“干到某个时间点就下班”,runMode:beforeDate: 是“只做一个任务,做完就下班”。最灵活的是第三种,可以自己控制循环。
10. 如何实现线程保活?常驻线程的应用场景及注意事项?
专业答案(含级别标注)
【初级掌握】线程保活就是让子线程的 RunLoop 一直运行,避免线程在执行完任务后被销毁。常见做法是在子线程中获取 RunLoop 并添加一个永不触发的 Source 或 Timer,然后调用 run。
【中级扩展】实现步骤:
- 在子线程中获取 RunLoop:
[NSRunLoop currentRunLoop]。 - 添加一个 Port 或 Source 保持 RunLoop 运行(如
[[NSMachPort alloc] init]并添加到 RunLoop)。 - 调用
[runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]在循环中运行。
应用场景:AFNetworking 的网络请求回调线程、后台持续监控任务(如日志上报)。
【高级深入】注意事项:
- 内存管理:常驻线程容易导致循环引用,需确保线程退出时释放资源。
- 正确停止:通过
CFRunLoopStop或移除所有 Source 让 RunLoop 退出。 - 避免在保活线程中执行耗时任务阻塞 RunLoop。
- 使用
runMode:beforeDate:而不是run,以便可以控制退出。
通俗解释
线程保活就是让一个子线程“永不眠”,一直待命执行任务。比如你请了一个专属服务员,他一直站在旁边等你的命令,而不是干完一个活就下班。在 iOS 中,通过给这个线程的 RunLoop 加一个“永远不响”的门铃(Port),让它一直等着,有任务就醒来做,做完继续等。
11. RunLoop 和 AutoreleasePool 有什么关系?
专业答案(含级别标注)
【初级掌握】RunLoop 每次循环开始前会创建一个自动释放池,循环结束时(BeforeWaiting 或 Exit)释放该池,从而管理临时对象的生命周期。
【中级扩展】系统在主线程 RunLoop 中注册了两个 Observer:
- 第一个 Observer 监听
kCFRunLoopEntry,调用_objc_autoreleasePoolPush创建池。 - 第二个 Observer 监听
kCFRunLoopBeforeWaiting(即将休眠)和kCFRunLoopExit(退出),调用_objc_autoreleasePoolPop释放旧池并创建新池。
这样确保每个循环中的临时对象在循环结束时被释放,避免内存堆积。
【高级深入】手动控制自动释放池的场景(如 for 循环大量创建临时对象)仍然需要@autoreleasepool块。理解 RunLoop 与 AutoreleasePool 的配合,有助于优化内存峰值和避免内存泄漏。
通俗解释
RunLoop 就像每天的工作循环,AutoreleasePool 就像下班前的垃圾清理。每次开始工作(进入 Loop)时放一个垃圾桶,工作中产生的临时垃圾(临时对象)都扔进去,下班前(休眠前)把垃圾桶倒掉。这样房间(内存)就不会被垃圾堆满。
12. RunLoop 和 GCD 的关系是怎样的?
专业答案(含级别标注)
【初级掌握】GCD 的任务调度不依赖 RunLoop,但主队列的任务执行需要 RunLoop 来驱动。当主队列有任务时,RunLoop 会被唤醒并执行。
【中级扩展】具体关系:
- 主队列的任务通过
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__在 RunLoop 的唤醒阶段执行。 - 其他队列(全局队列、自定义串行/并发队列)由 GCD 管理的线程池执行,不涉及 RunLoop(除非在子线程中手动开启 RunLoop)。
- RunLoop 的休眠唤醒机制基于
mach_msg,而 GCD 的dispatch_source也可作为 RunLoop 的事件源(Source1)。
【高级深入】GCD 和 RunLoop 可以协同工作,例如使用dispatch_source创建定时器,将其事件封装为 RunLoop Source;或者在 RunLoop 线程中调用dispatch_async将任务派发到其他队列。高级开发者需理解两者在底层如何通过内核通信。
通俗解释
RunLoop 是主线程的“管家”,负责处理各种家务(事件)。GCD 是“任务调度中心”,可以派活到不同的人(线程)。当 GCD 给主线程派活时,它会通过敲窗(mach_msg)叫醒管家(RunLoop),管家醒来后就去执行这个任务。
13. 什么是 CommonModes?它和具体 Mode 有什么区别?
专业答案(含级别标注)
【初级掌握】CommonModes 不是一个具体的 Mode,而是一个标记,表示将某个 Source/Timer/Observer 添加到一组 Mode 中。默认的 CommonModes 集合包含 NSDefaultRunLoopMode 和 UITrackingRunLoopMode。
【中级扩展】区别:
- 具体 Mode(如 Default、Tracking)是独立的运行环境,RunLoop 一次只能运行在一个具体 Mode 下。
- CommonModes 是一个集合,向 CommonModes 添加事件源,相当于同时添加到集合内的所有具体 Mode 中。
- 可以通过
CFRunLoopAddCommonMode将自定义 Mode 加入 CommonModes 集合。
【高级深入】CommonModes 的实现是通过__CFRunLoop结构中的_commonModes集合和_commonModeItems映射实现的。当一个 Mode 被加入 CommonModes 时,RunLoop 会将当前 CommonModes 中的所有事件源复制一份到这个新 Mode 中。
通俗解释
具体 Mode 就像一个个独立的房间(办公室、会议室),CommonModes 就像一张“万能门禁卡”,拥有这张卡的人(Timer)可以进入所有被标记的房间。默认这张卡能进办公室和会议室。
14. 如何监控 App 卡顿?基于 RunLoop 的实现原理?
专业答案(含级别标注)
【初级掌握】通过监听 RunLoop 的状态,计算两次状态之间的耗时,如果超过阈值(如 50ms),则认为发生卡顿。
【中级扩展】实现步骤:
- 创建一个 CFRunLoopObserver 监听
kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting。 - 在回调中记录时间戳,并在子线程中轮询检查(如使用 dispatch_after)时间差。
- 如果从 BeforeSources 到 BeforeWaiting 耗时过长,或在 BeforeWaiting 后长时间未进入下一次循环,则判定为卡顿,记录堆栈。
常用工具如 Facebook 的FBRetainCycleDetector中的卡顿监控模块。
【高级深入】更精准的实现:使用mach_thread_info获取线程 CPU 使用率,结合 RunLoop 耗时分析。高级监控工具还需考虑堆栈符号化、循环检测、卡顿分级等。注意 Observer 本身不能过于耗时,否则会影响性能。
通俗解释
卡顿监控就像在管家(RunLoop)身上装了一个计时器,看他从开始干活(BeforeSources)到准备休息(BeforeWaiting)花了多长时间。如果超过正常范围(比如 50ms),说明他可能被某个任务卡住了,我们就记录下当时的情况(堆栈)来找出元凶。
15. RunLoop 在实际开发中有哪些常见的应用场景?
专业答案(含级别标注)
【初级掌握】定时器、事件响应、界面刷新。
【中级扩展】常见场景:
- 线程保活:AFNetworking 的常驻线程。
- 滑动流畅性优化:将非必要任务放在 DefaultMode,避免影响 TrackingMode。
- 卡顿监控:监听 RunLoop 状态耗时。
-
PerformSelector 系列方法:
performSelector:withObject:afterDelay:依赖 Timer。 - 自动释放池管理:RunLoop 循环自动管理池。
-
异步任务延迟执行:利用
CFRunLoopPerformBlock。
【高级深入】高级场景: - 自定义 Mode:实现特定任务隔离,如后台下载任务独立 Mode。
- CADisplayLink:基于 RunLoop 实现同步刷新。
- RunLoop 与 XCTest:测试中的等待机制。
- RunLoop 与 CoreFoundation 网络编程:CFFTP、CFSocket 等。
通俗解释
RunLoop 就像 iOS 系统的“万能工具箱”,几乎所有需要循环等待、处理事件的场景都有它的影子。从最简单的点击屏幕,到复杂的网络请求回调,再到性能监控,都离不开它。
16. PerformSelector 和 RunLoop 的关系?
专业答案(含级别标注)
【初级掌握】performSelector:withObject:afterDelay: 是基于 Timer 实现的,需要将线程的 RunLoop 运行在默认模式下才能触发。
【中级扩展】具体关系:
-
performSelector:withObject:afterDelay:会在当前线程的 RunLoop 中创建一个 Timer(默认模式),延时后执行。 -
performSelectorOnMainThread:withObject:waitUntilDone:通过 Source0 或 GCD 在主线程 RunLoop 中执行。 -
performSelector:onThread:withObject:waitUntilDone:在目标线程的 RunLoop 中执行,若目标线程没有 RunLoop 则无效。
【高级深入】底层实现:带afterDelay的方法使用CFRunLoopTimerCreate;不带延时的主线程调用通过dispatch_async或 Source0;带waitUntilDone的会通过信号量阻塞当前线程直到执行完成。
通俗解释
PerformSelector 就像给未来的自己打电话。如果你说“3 分钟后提醒我”,系统就会设置一个闹钟(Timer)到 RunLoop 里,时间到了就执行。如果你说“让主线程去做”,系统就会给主线程的 RunLoop 发一条消息(Source0)。
17. 为什么说 RunLoop 不适合高精度实时任务?
专业答案(含级别标注)
【初级掌握】因为 RunLoop 的 Timer 依赖于循环检查,可能会被其他事件延迟,精度不高。
【中级扩展】原因:
- Timer 的触发是在每次 RunLoop 循环中检查的,如果当前循环有耗时任务,Timer 会被延后。
- Mode 切换时,不在当前 Mode 的 Timer 会被暂时忽略,导致延迟。
- RunLoop 的休眠唤醒依赖于内核调度,存在不确定性。
【高级深入】对于高精度需求(如音视频同步),应使用 GCD 的dispatch_source或实时线程(Realtime Thread)。dispatch_source基于内核时钟,精度更高且不受 RunLoop 模式影响。也可使用mach_wait_until实现忙等,但会消耗 CPU。
通俗解释
RunLoop 的定时器就像用沙漏计时,如果中间有人叫你去做别的事(处理触摸),沙漏就会暂停,导致时间不准。对于需要精确到毫秒的任务,得用更精准的计时器(GCD 定时器),它像电子表,不受干扰。
18. 如果 RunLoop 的 Mode 中没有事件源会发生什么?
专业答案(含级别标注)
【初级掌握】如果 Mode 里没有任何 Source、Timer、Observer,RunLoop 会立即退出,不会进入休眠。
【中级扩展】RunLoop 在启动时会检查当前 Mode 是否为空,如果为空则直接返回 false,不会进入循环。这也是为什么子线程的 RunLoop 需要至少添加一个事件源才能持续运行的原因。
【高级深入】可以通过 CFRunLoopRunInMode 返回值判断是否退出。在实现线程保活时,必须确保 Mode 中始终有至少一个事件源(如一个永远不触发的 Port),否则 RunLoop 会立即退出,线程也随之销毁。
通俗解释
如果一个房间(Mode)里没有任何事情可做(没有电话、没有闹钟、没有需要盯着的屏幕),那你待在里面干嘛?当然就出来了。RunLoop 也是这样,没活干就退出。
19. RunLoop 如何与 CoreAnimation 协同工作?
专业答案(含级别标注)
【初级掌握】CoreAnimation 的动画提交和渲染依赖于 RunLoop。在 RunLoop 每次循环即将休眠(BeforeWaiting)时,会提交 CATransaction,执行动画更新。
【中级扩展】具体流程:
- 当修改 layer 属性时,会自动触发
setNeedsDisplay或setNeedsLayout,并隐式提交一个 CATransaction。 - CATransaction 会在当前 RunLoop 循环结束前(BeforeWaiting)提交所有动画变更到渲染服务。
- RunLoop 通过 Observer 监听
kCFRunLoopBeforeWaiting和kCFRunLoopExit来提交事务。
【高级深入】可以通过CATransaction的begin/commit手动控制事务。理解 RunLoop 与 CoreAnimation 的配合,有助于优化动画性能,避免掉帧。
通俗解释
CoreAnimation 就像动画导演,RunLoop 就像场记。导演(CoreAnimation)把每一帧要做的改动记录下来(CATransaction),场记(RunLoop)在每次休息前(BeforeWaiting)把记录交给舞台(渲染服务)去执行,保证动画连贯。
20. 如何正确停止一个正在运行的 RunLoop?
专业答案(含级别标注)
【初级掌握】调用 CFRunLoopStop 可以停止当前 RunLoop。
【中级扩展】注意事项:
-
CFRunLoopStop仅对本次 run 调用有效,如果 RunLoop 是通过run启动的,停止后无法再次运行;如果是通过runMode:beforeDate:循环启动,停止后退出当前循环,外层循环可继续。 - 也可以移除 RunLoop 中的所有事件源,使 RunLoop 自然退出。
- 对于主线程 RunLoop,不应该手动停止,因为系统需要它持续运行。
【高级深入】在实现线程保活时,常用一个标志位 +runMode:beforeDate:循环,当需要停止时,设置标志位并调用CFRunLoopStop退出当前循环,外层循环检测标志位后不再继续。
通俗解释
停止 RunLoop 就像给正在工作的人喊“下班了”。可以直接喊停(CFRunLoopStop),也可以把办公室里所有事情都清空(移除事件源),他自然就下班了。注意老板(主线程)不能随便喊下班,否则公司就倒闭了。
三、初中高工程师回答指南
| 级别 | 回答要点 |
|---|---|
| 初级 | 能够说出 RunLoop 的基本概念(保持程序运行、处理事件、线程关系),知道 Timer 需要添加到 RunLoop 才能工作,能简单说明滑动时 Timer 不响应的原因(因为 Mode 切换),并给出基本解决方案(添加到 CommonModes)。对内部实现不了解。 |
| 中级 | 除了初级内容,能详细描述 RunLoop 的核心类(Source/Timer/Observer)、Mode 的作用和常见 Mode,能画出或说出 RunLoop 运行的主要流程(11 步大致顺序),理解休眠实现基于 mach_msg。能结合实际项目举例(如 AFNetworking 常驻线程、监控卡顿原理)。能分析 NSTimer 失效的深层原因并给出多种解决方案。 |
| 高级 | 对 RunLoop 源码有研究,能深入讲解 CFRunLoop 的底层数据结构(如 __CFRunLoop、__CFRunLoopMode、__CFRunLoopSource 等),理解 mach_msg 在内核态和用户态切换的细节,知道 RunLoop 如何与 GCD、AutoreleasePool 交互。能设计基于 RunLoop 的性能监控工具(如检测卡顿、帧率),能优化复杂场景下的 RunLoop 使用(如自定义 Mode、线程保活中的内存管理)。对 RunLoop 的局限性和替代方案(如 GCD 定时器、Dispatch Source)也有清晰认识。 |
四、英文术语速查表
| 中文 | 英文 | 简短说明 |
|---|---|---|
| 运行循环 | RunLoop | 线程的事件处理循环 |
| 运行模式 | Mode | RunLoop 的多种工作场景,如默认、追踪 |
| 事件源 | Source | 产生事件的对象,分 Source0 和 Source1 |
| 用户事件源 | Source0 | 非基于端口的事件,如触摸、performSelector |
| 端口事件源 | Source1 | 基于端口的事件,用于唤醒 RunLoop |
| 定时器 | Timer | 基于时间的触发器,如 NSTimer |
| 观察者 | Observer | 监听 RunLoop 状态变化 |
| 进入 Loop | kCFRunLoopEntry | RunLoop 即将开始 |
| 即将处理定时器 | kCFRunLoopBeforeTimers | 即将处理 Timer 事件 |
| 即将处理事件源 | kCFRunLoopBeforeSources | 即将处理 Source 事件 |
| 即将休眠 | kCFRunLoopBeforeWaiting | 即将进入休眠状态 |
| 刚唤醒 | kCFRunLoopAfterWaiting | 刚从休眠中唤醒 |
| 退出 Loop | kCFRunLoopExit | RunLoop 即将结束 |
| 通用模式集合 | CommonModes | 一组 Mode 的标记,使对象在多个 Mode 中有效 |
| 内核消息 | mach_msg | XNU 内核的 IPC 机制,用于休眠唤醒 |
| 线程保活 | Thread KeepAlive | 使子线程持续运行,不自动退出 |
| 卡顿监控 | Lag Monitoring | 通过 RunLoop 耗时检测界面卡顿 |
文档说明:
本文档整合了 RunLoop 的完整知识体系,包括思维导图、20道面试题(含分级答案和通俗解释)、工程师回答指南和术语速查表,可作为 iOS 开发者学习、面试准备和技术分享的权威参考。









网友评论