RunLoop-思维导图、面试题及其答案

2026-03-04  本文已影响0人  巴糖

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 在程序启动时自动运行。
【中级扩展】实际开发中常见应用包括:

通俗解释
RunLoop 就像程序的心脏,不停跳动,有任务时处理,没任务时休息。主线程的 RunLoop 保证了 App 能一直响应你的操作。在项目中,我用它来让一个后台线程常驻(比如网络请求回调线程),避免频繁创建销毁线程(线程保活);也用它解决过滑动时定时器暂停的问题(将 Timer 加入 CommonModes);还用它监控 App 是否卡顿(通过监听 RunLoop 状态耗时)。


2. RunLoop 内部实现逻辑?

专业答案(含级别标注)
【初级掌握】RunLoop 内部是一个 do-while 循环,每次循环会检查事件并处理,没有事件时休眠。
【中级扩展】具体步骤如下:

  1. 通知观察者即将进入 RunLoop(kCFRunLoopEntry)。
  2. 通知观察者即将处理 Timer 事件(kCFRunLoopBeforeTimers)。
  3. 通知观察者即将处理 Source 事件(kCFRunLoopBeforeSources)。
  4. 处理 Blocks(非延迟的 block)。
  5. 处理 Source0(触摸事件、performSelector 等用户触发的事件,可能会再次触发 Blocks)。
  6. 如果有 Source1(基于端口的通信),跳转到第 8 步立即处理;否则继续。
  7. 通知观察者线程即将休眠(kCFRunLoopBeforeWaiting),并调用 mach_msg 进入内核态等待消息。
  8. 被唤醒后通知观察者(kCFRunLoopAfterWaiting),然后处理唤醒原因:
    • 处理 Timer(定时器到期)
    • 处理 GCD 异步主队列任务(__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
    • 处理 Source1(端口消息)
  9. 处理 Blocks。
  10. 根据结果决定继续循环或退出(如超时、停止指令等)。
  11. 通知观察者退出 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 是一个集合,默认包含 NSDefaultRunLoopModeUITrackingRunLoopMode,这样 Timer 在两个 Mode 下都能工作。
【中级扩展】另外两种方案:

  1. 在子线程中创建 Timer:子线程 RunLoop 不受主线程滑动影响,但需注意线程保活和 UI 更新需切回主线程。
  2. 使用 CADisplayLink 或 GCD 定时器:CADisplayLink 也受 Mode 影响,同样可添加到 CommonModes;GCD 定时器基于队列,不依赖 RunLoop,但要注意循环引用和线程安全。
    推荐方案:使用第一种,简单有效。
    【高级深入】深入分析:NSTimer 依赖于 RunLoop 的时间点检查,而 RunLoop 在 UITrackingRunLoopMode 下不处理默认模式的 Timer,这是设计使然。高级解决方案包括自定义 Mode 并控制其切换,或者利用 CFRunLoop 的 CFRunLoopAddTimer 精细管理。对于高精度定时需求,可采用 mach_absolute_time 结合 dispatch_source 实现。

通俗解释
这是典型的“房间切换”问题。滑动时系统切换到“追踪房间”,而你的闹钟在“默认房间”,所以听不到。解决办法:

  1. 把闹钟放到“通用房间”(CommonModes),这样在默认和追踪房间都能听到。
  2. 把闹钟放到另一个房间(子线程),但要注意子线程需要开启 RunLoop 且 UI 更新必须切回主线程。
  3. 换一个不用房间的闹钟,比如 GCD 定时器,它不依赖 RunLoop。

6. RunLoop 是怎么响应用户操作的,具体流程是什么样的?

专业答案(含级别标注)
【初级掌握】用户操作通过 Source0 和 Source1 协同处理:Source1 负责唤醒 RunLoop,Source0 负责具体事件分发。
【中级扩展】详细流程:

  1. 用户触摸屏幕,硬件触发中断,系统生成 IOHIDEvent
  2. 通过 mach_msg 传递给应用进程的 Source1(基于端口的通信)。
  3. RunLoop 在休眠中被唤醒,处理 Source1,将事件交给 _UIApplicationHandleEventQueue
  4. 系统将事件转换为 UIEvent,并通过 Source0 回调(__IOHIDEventSystemClientQueueCallback)分发到 UIWindow,最终找到合适的响应者处理。
  5. 事件处理过程中可能触发 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 种状态:

通俗解释
RunLoop 在运行过程中会经历几个关键节点,就像人的一天:起床(Entry)、准备吃饭(BeforeTimers)、准备工作(BeforeSources)、准备睡觉(BeforeWaiting)、被叫醒(AfterWaiting)、睡觉(Exit)。我们可以通过 Observer 来监听这些节点,比如在准备睡觉时做点事情(如自动释放池释放)。


8. RunLoop 的 Mode 作用是什么?

专业答案(含级别标注)
【初级掌握】Mode 用于隔离不同组的事件源,使 RunLoop 可以在不同场景下选择性地处理事件,避免互相干扰。常见 Mode 有 NSDefaultRunLoopModeUITrackingRunLoopMode
【中级扩展】每个 Mode 包含一组 Source、Timer、Observer,RunLoop 启动时只能选择其中一个 Mode 运行。切换 Mode 必须退出当前 Loop,再重新进入另一个 Mode。作用包括:

通俗解释
Mode 就像不同的工作场景:你在办公室(DefaultMode)处理邮件,在会议室(TrackingMode)专注演示。如果会议中突然有邮件提醒,你会先忽略(不处理 DefaultMode 的事件),等会议结束再处理。这样保证了当前场景的流畅性。CommonModes 则是一个“VIP 通行证”,让你的人(Timer)可以在多个场景中出现。


9. RunLoop 有哪些启动方式?有什么区别?

专业答案(含级别标注)
【初级掌握】RunLoop 有三种启动方式:runrunUntilDate:runMode:beforeDate:
【中级扩展】区别:

通俗解释
启动 RunLoop 就像开启一个工作模式:run 是“一直干到死”(不推荐),runUntilDate: 是“干到某个时间点就下班”,runMode:beforeDate: 是“只做一个任务,做完就下班”。最灵活的是第三种,可以自己控制循环。


10. 如何实现线程保活?常驻线程的应用场景及注意事项?

专业答案(含级别标注)
【初级掌握】线程保活就是让子线程的 RunLoop 一直运行,避免线程在执行完任务后被销毁。常见做法是在子线程中获取 RunLoop 并添加一个永不触发的 Source 或 Timer,然后调用 run
【中级扩展】实现步骤:

  1. 在子线程中获取 RunLoop:[NSRunLoop currentRunLoop]
  2. 添加一个 Port 或 Source 保持 RunLoop 运行(如 [[NSMachPort alloc] init] 并添加到 RunLoop)。
  3. 调用 [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]] 在循环中运行。
    应用场景:AFNetworking 的网络请求回调线程、后台持续监控任务(如日志上报)。
    【高级深入】注意事项:

通俗解释
线程保活就是让一个子线程“永不眠”,一直待命执行任务。比如你请了一个专属服务员,他一直站在旁边等你的命令,而不是干完一个活就下班。在 iOS 中,通过给这个线程的 RunLoop 加一个“永远不响”的门铃(Port),让它一直等着,有任务就醒来做,做完继续等。


11. RunLoop 和 AutoreleasePool 有什么关系?

专业答案(含级别标注)
【初级掌握】RunLoop 每次循环开始前会创建一个自动释放池,循环结束时(BeforeWaiting 或 Exit)释放该池,从而管理临时对象的生命周期。
【中级扩展】系统在主线程 RunLoop 中注册了两个 Observer:

通俗解释
RunLoop 就像每天的工作循环,AutoreleasePool 就像下班前的垃圾清理。每次开始工作(进入 Loop)时放一个垃圾桶,工作中产生的临时垃圾(临时对象)都扔进去,下班前(休眠前)把垃圾桶倒掉。这样房间(内存)就不会被垃圾堆满。


12. RunLoop 和 GCD 的关系是怎样的?

专业答案(含级别标注)
【初级掌握】GCD 的任务调度不依赖 RunLoop,但主队列的任务执行需要 RunLoop 来驱动。当主队列有任务时,RunLoop 会被唤醒并执行。
【中级扩展】具体关系:

通俗解释
RunLoop 是主线程的“管家”,负责处理各种家务(事件)。GCD 是“任务调度中心”,可以派活到不同的人(线程)。当 GCD 给主线程派活时,它会通过敲窗(mach_msg)叫醒管家(RunLoop),管家醒来后就去执行这个任务。


13. 什么是 CommonModes?它和具体 Mode 有什么区别?

专业答案(含级别标注)
【初级掌握】CommonModes 不是一个具体的 Mode,而是一个标记,表示将某个 Source/Timer/Observer 添加到一组 Mode 中。默认的 CommonModes 集合包含 NSDefaultRunLoopModeUITrackingRunLoopMode
【中级扩展】区别:

通俗解释
具体 Mode 就像一个个独立的房间(办公室、会议室),CommonModes 就像一张“万能门禁卡”,拥有这张卡的人(Timer)可以进入所有被标记的房间。默认这张卡能进办公室和会议室。


14. 如何监控 App 卡顿?基于 RunLoop 的实现原理?

专业答案(含级别标注)
【初级掌握】通过监听 RunLoop 的状态,计算两次状态之间的耗时,如果超过阈值(如 50ms),则认为发生卡顿。
【中级扩展】实现步骤:

  1. 创建一个 CFRunLoopObserver 监听 kCFRunLoopBeforeSourceskCFRunLoopBeforeWaiting
  2. 在回调中记录时间戳,并在子线程中轮询检查(如使用 dispatch_after)时间差。
  3. 如果从 BeforeSources 到 BeforeWaiting 耗时过长,或在 BeforeWaiting 后长时间未进入下一次循环,则判定为卡顿,记录堆栈。
    常用工具如 Facebook 的 FBRetainCycleDetector 中的卡顿监控模块。
    【高级深入】更精准的实现:使用 mach_thread_info 获取线程 CPU 使用率,结合 RunLoop 耗时分析。高级监控工具还需考虑堆栈符号化、循环检测、卡顿分级等。注意 Observer 本身不能过于耗时,否则会影响性能。

通俗解释
卡顿监控就像在管家(RunLoop)身上装了一个计时器,看他从开始干活(BeforeSources)到准备休息(BeforeWaiting)花了多长时间。如果超过正常范围(比如 50ms),说明他可能被某个任务卡住了,我们就记录下当时的情况(堆栈)来找出元凶。


15. RunLoop 在实际开发中有哪些常见的应用场景?

专业答案(含级别标注)
【初级掌握】定时器、事件响应、界面刷新。
【中级扩展】常见场景:

通俗解释
RunLoop 就像 iOS 系统的“万能工具箱”,几乎所有需要循环等待、处理事件的场景都有它的影子。从最简单的点击屏幕,到复杂的网络请求回调,再到性能监控,都离不开它。


16. PerformSelector 和 RunLoop 的关系?

专业答案(含级别标注)
【初级掌握】performSelector:withObject:afterDelay: 是基于 Timer 实现的,需要将线程的 RunLoop 运行在默认模式下才能触发。
【中级扩展】具体关系:

通俗解释
PerformSelector 就像给未来的自己打电话。如果你说“3 分钟后提醒我”,系统就会设置一个闹钟(Timer)到 RunLoop 里,时间到了就执行。如果你说“让主线程去做”,系统就会给主线程的 RunLoop 发一条消息(Source0)。


17. 为什么说 RunLoop 不适合高精度实时任务?

专业答案(含级别标注)
【初级掌握】因为 RunLoop 的 Timer 依赖于循环检查,可能会被其他事件延迟,精度不高。
【中级扩展】原因:

通俗解释
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,执行动画更新。
【中级扩展】具体流程:

通俗解释
CoreAnimation 就像动画导演,RunLoop 就像场记。导演(CoreAnimation)把每一帧要做的改动记录下来(CATransaction),场记(RunLoop)在每次休息前(BeforeWaiting)把记录交给舞台(渲染服务)去执行,保证动画连贯。


20. 如何正确停止一个正在运行的 RunLoop?

专业答案(含级别标注)
【初级掌握】调用 CFRunLoopStop 可以停止当前 RunLoop。
【中级扩展】注意事项:

通俗解释
停止 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 开发者学习、面试准备和技术分享的权威参考。

上一篇 下一篇

猜你喜欢

热点阅读