全文共分为七大章节,从背景由来、模型剖析、源码解读、运行流程、实战应用、调试工具到面试题总结,建议先收藏,慢慢消化。


很多 iOS 开发者写了几年代码,对 RunLoop 的印象可能还停留在:
“就是那个让程序不死的死循环。”
“滑动的时候 NSTimer 会停,加个 CommonModes 就好了。”
但这些理解太浅了。为什么 RunLoop 能让线程休眠却不占 CPU?它是如何优雅地处理触摸、网络、定时器的?为什么 AutoreleasePool 的释放时机和 RunLoop 有关?
今天,我们就把 RunLoop 从黑盒拆成白盒。
一、背景由来:从 C 语言的 return 0 到 iOS 的“永远在线”

1.1 命令行程序 vs GUI 程序
我们学编程的第一个 C 程序:
#include<stdio.h>intmain(){printf("Hello, World!\n");return 0; // 程序执行完毕,退出}
这段代码执行完 printf后,进程就结束了。这是典型的 “执行-退出” 模型。
但 iOS App 不一样。你点击 App 图标后,它不会自己退出,而是一直待在那里,等你触摸、滑动、输入。这是一个 “事件驱动” 模型。

1.2 iOS App 的启动入口:UIApplicationMain
打开任何一个 iOS 项目的 main.m 文件,你会看到:
int main(int argc, char * argv[]) {@autoreleasepool {return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));}}
UIApplicationMain 函数做了三件核心事情:
1. 创建 UIApplication单例。
2. 创建 AppDelegate实例并赋值给 UIApplication。
3. 开启主线程的 RunLoop。
这个函数一旦调用,就永远不会返回(除非 App 被系统杀死)。它内部启动了一个 RunLoop,让主线程进入“等待-处理-休眠”的无限循环中。

1.3 如果没有 RunLoop 会怎样?
我们做个实验:在子线程执行一个任务,任务结束后线程就销毁了。
let thread = Thread {print("子线程开始执行任务")// 任务执行完毕print("子线程任务结束")}thread.start()
输出:
子线程开始执行任务子线程任务结束
线程完成任务后自动退出。如果我们想让这个线程一直活着,随时接收任务呢?
let thread = Thread {print("子线程启动")// 开启 RunLoop,让线程永远不退出RunLoop.current.add(Port(), forMode: .default)RunLoop.current.run()print("这行代码永远不会执行")}thread.start()
这时候线程就不会退出了,它会一直等待消息。这就是 RunLoop 最核心的能力:线程保活。
二、RunLoop 核心模型:五大组件的协同作战

RunLoop 并不是一个简单的 while 循环,它是一个由五个核心组件构成的精密系统。
组件 | C 语言类型 | 作用 |
RunLoop | CFRunLoopRef | 管理线程事件循环的总控制器 |
Mode | CFRunLoopModeRef | 运行模式,决定了当前循环监听哪些事件 |
Source | CFRunLoopSourceRef | 事件源,分为 Source0 和 Source1 |
Timer | CFRunLoopTimerRef | 基于时间的触发器 |
Observer | CFRunLoopObserverRef | 监听 RunLoop 各个阶段的状态变化 |
我们打开苹果开源的CoreFounation源码(CFRunLoop.c),可以看到 RunLoop 的核心数据结构:
struct __CFRunLoop {CFRuntimeBase _base;pthread_mutex_t _lock; // 锁,用于保护数据结构__CFPort _wakeUpPort; // 唤醒端口,用于内核唤醒Boolean _unused;volatile _per_run_data *_perRunData; // 每次运行重置的数据pthread_t _pthread; // 对应的线程uint32_t _winthread;CFMutableSetRef _commonModes; // 标记为 Common 的 Mode 集合CFMutableSetRef _commonModeItems; // CommonMode 共享的 Source/Timer/ObserverCFRunLoopModeRef _currentMode; // 当前运行的 ModeCFMutableSetRef _modes; // 所有的 Mode 集合// ...};
RunLoop 和线程是一一对应的,通过一个全局字典__CFRunLoops
来管理:// 获取当前线程的 RunLoop 的内部实现(简化版)CFRunLoopRef CFRunLoopGetCurrent(void) {// 从线程局部存储(TLS)中获取 RunLoop 对象CFRunLoopRef loop = (CFRunLoopRef)pthread_getspecific(__CFRunLoopKey);if (!loop) {// 如果不存在,则创建一个新的loop = _CFRunLoopCreate();pthread_setspecific(__CFRunLoopKey, loop);}return loop;}
三、RunLoop 核心机制深度解析

3.1 Mode:工作模式的奥秘
RunLoop 在任一时刻只能运行在一个 Mode 下,切换 Mode 必须退出当前循环。
struct __CFRunLoopMode {CFRuntimeBase _base;pthread_mutex_t _lock;CFStringRef _name; // Mode 名称Boolean _stopped;CFMutableSetRef _sources0; // Source0 集合CFMutableSetRef _sources1; // Source1 集合CFMutableArrayRef _observers; // Observer 数组CFMutableArrayRef _timers; // Timer 数组// ...};
苹果预定义了五种 Mode,但公开给我们用的主要是这几种:
Mode | 值 | 说明 |
NSDefaultRunLoopMode | kCFRunLoopDefaultMode | 默认模式,App 空闲时处于此模式 |
UITrackingRunLoopMode | UITrackingRunLoopMode | UI 追踪模式,滑动 ScrollView 时切换 |
NSRunLoopCommonModes | kCFRunLoopCommonModes | 伪模式,只是一个标记集合 |
GSEventReceiveRunLoopMode | - | 接收系统事件(私有) |
UIInitializationRunLoopMode | - | App 启动时的第一个 Mode(私有) |
经典面试题:NSRunLoopCommonModes 是真正的 Mode 吗?
不是。它只是一个字符串集合,你可以将多个 Mode 标记为“Common”。当 RunLoop 运行在任何一个被标记为 Common 的 Mode 下时,注册在NSRuLoopCommonModes下的Source/Timer/Observer都会被处理。
源码实现:
void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName) {// 将 modeName 添加到 _commonModes 集合中CFSetAddValue(rl->_commonModes, modeName);// 将该 Mode 下的所有 Item 同步到 _commonModeItems 中}

3.2 Source:事件驱动的源头
Source 是 RunLoop 的数据输入源,分为两类:
Source0:手动触发
只包含一个回调函数指针,不能主动唤醒 RunLoop。
需要先调用 CFRunLoopSourceSignal(source) 标记为待处理,然后调用 CFRunLoopWakeUp(runloop) 手动唤醒。
典型场景:触摸事件、performSelector:onThread:...
// 创建 Source0 的简化演示var context = CFRunLoopSourceContext()context.perform = { (info) inprint("Source0 被触发了")}let source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context)CFRunLoopAddSource(CFRunLoopGetCurrent(), source, .defaultMode)// 触发CFRunLoopSourceSignal(source)CFRunLoopWakeUp(CFRunLoopGetCurrent())
Source1:内核触发
包含一个 Mach Port 和一个回调函数。
由内核或其他线程通过 Mach Port 发送消息触发,能自动唤醒 RunLoop。
典型场景:网络数据到达、硬件事件、GCD 的 Main Queue
Source1 是 RunLoop 能够休眠并被唤醒的关键,因为它依赖 Mach Port与内核通信。

3.3 Mach Port 与休眠机制
RunLoop 休眠时并不是调用 sleep(),而是调用 mach_msg()系统调用。
// CFRunLoopRun 的核心代码(简化)static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, ...) {do {// 1. 通知 Observer:即将处理 Timer/Source__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);// 2. 处理 Blocks__CFRunLoopDoBlocks(rl, rlm);// 3. 处理 Source0if (__CFRunLoopDoSources0(rl, rlm)) {__CFRunLoopDoBlocks(rl, rlm);}// 4. 如果有 Source1 待处理,跳转到处理步骤if (__CFRunLoopServiceMachPort(dispatchPort, &msg, ...)) {goto handle_msg;}// 5. 通知 Observer:即将休眠__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);// 6. 🔥 核心:调用 mach_msg 等待内核消息,线程在此休眠// 当没有消息时,线程被挂起,CPU 占用为 0__CFRunLoopServiceMachPort(waitSet, &msg, ...);// 7. 被唤醒后,通知 Observer:结束休眠__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);handle_msg:// 8. 处理唤醒事件if (msg是Timer) {__CFRunLoopDoTimers(rl, rlm);} else if (msg是GCD) {__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();} else if (msg是Source1) {__CFRunLoopDoSource1(rl, rlm, source);}// 9. 处理 Blocks__CFRunLoopDoBlocks(rl, rlm);// 10. 判断是否继续循环} while (结果不为0 && 未停止);}
mach_msg() 是一个系统调用,它会让当前线程从用户态切换到内核态。内核会检查 Mach Port 的消息队列:
如果有消息:立即返回,线程继续执行。
如果没有消息:将线程挂起,CPU 时间片分配给其他线程,当前线程几乎不耗电。
这就是 RunLoop 能做到“有活干,没活睡”的底层原理。

3.4 Timer:定时器的底层实现
NSTimer其实就是 CFRunLoopTimerRef的上层封装。当我们创建一个 Timer 并添加到 RunLoop 时:
let timer = Timer(timeInterval: 1.0, repeats: true) { _ inprint("Timer fired")}RunLoop.current.add(timer, forMode: .default)
RunLoop 内部会计算所有 Timer 的下一次触发时间,然后在调用 mach_msg() 时,设置一个超时时间(timeout)。这个超时时间等于距离最近一个 Timer 触发的时间间隔。
// 计算休眠时长CFTimeInterval sleepInterval = __CFRunLoopGetNextTimerFireDate(rl, rlm);// 调用 mach_msg,最多等待 sleepInterval 秒__CFRunLoopServiceMachPort(waitSet, &msg, sleepInterval, ...);
这样,即使没有任何 Source1 事件,RunLoop 也会在 Timer 触发时被内核准时唤醒。
为什么滑动时 NSTimer 会暂停?
当用户滑动 UIScrollView 时,系统为了确保滑动的流畅度,会将主线程 RunLoop的 Mode 从 NSDefaultRunLoopMode切换为UITrackingRunLoopMode。而默认的Timer只添加在NSDefaultRunLoopMode 下,因此在滑动期间不会被处理。
解决方案:
RunLoop.current.add(timer, forMode: .common) // 添加到 CommonModes
3.5 Observer:RunLoop 的监控器
Observer 可以监听 RunLoop 的七个状态变化:
状态 | 枚举值 | 说明 |
Entry | kCFRunLoopEntry | 即将进入 RunLoop |
BeforeTimers | kCFRunLoopBeforeTimers | 即将处理 Timer |
BeforeSources | kCFRunLoopBeforeSources | 即将处理 Source |
BeforeWaiting | kCFRunLoopBeforeWaiting | 即将休眠 |
AfterWaiting | kCFRunLoopAfterWaiting | 刚被唤醒 |
Exit | kCFRunLoopExit | 即将退出 RunLoop |
AllActivities | - | 监听所有状态 |
实战应用:利用 Observer 实现卡顿检测
原理:如果主线程 RunLoop 在BeforeSources到 BeforeWaiting之间的耗时超过阈值(比如50ms), 就认为发生了一次卡顿。
class LagMonitor {private var observer: CFRunLoopObserver?private var activitySemaphore: DispatchSemaphore?private var timeoutCount = 0func startMonitoring() {let observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault,CFRunLoopActivity.allActivities.rawValue,true,0) { [weak self] observer, activity inself?.handleActivity(activity)}CFRunLoopAddObserver(CFRunLoopGetMain(), observer, .commonModes)self.observer = observer// 开启一个子线程,定时检测主线程 RunLoop 状态DispatchQueue.global().async {while true {let semaphore = DispatchSemaphore(value: 0)self.activitySemaphore = semaphore// 等待 50mslet result = semaphore.wait(timeout: .now() + 0.05)if result == .timedOut {// 超时未收到回调,说明主线程可能卡住了if self.timeoutCount < 5 {self.timeoutCount += 1continue}// 连续超时,记录卡顿堆栈self.recordLagStackTrace()}self.timeoutCount = 0}}}private func handleActivity(_ activity: CFRunLoopActivity) {// 每当 RunLoop 状态变化,就发信号,让子线程知道主线程还在正常工作activitySemaphore?.signal()}private func recordLagStackTrace() {// 获取主线程调用栈,记录卡顿发生位置let symbols = Thread.callStackSymbolsprint("⚠️ 检测到卡顿,堆栈:\(symbols)")}}
这是很多性能监控SDK (如腾讯的 MLeaksFinder、美团的 Performance-Monitor) 的核心原理。
四、RunLoop 完整运行流程图解

结合上面的源码分析,我们可以画出 RunLoop 一次完整循环的详细流程:

五、RunLoop 经典应用场景与代码实战

5.1 线程保活(常驻线程)
AFNetworking2.x版本曾使用 RunLoop 实现常驻线程,用于后台网络回调处理。
class PermanentThread: NSObject {private var thread: Thread?private var runLoop: RunLoop?override init() {super.init()thread = Thread(block: { [weak self] in// 必须添加一个 Source/Timer/Observer,否则 RunLoop 会立即退出self?.runLoop = RunLoop.currentself?.runLoop?.add(Port(), forMode: .default)// 开启 RunLoopwhile !Thread.current.isCancelled {self?.runLoop?.run(mode: .default, before: Date.distantFuture)}print("线程退出")})thread?.start()}func executeTask(_ task: @escaping () -> Void) {guard let runLoop = runLoop else { return }DispatchQueue.main.async {// 将任务提交到常驻线程执行CFRunLoopPerformBlock(runLoop.getCFRunLoop(), .default, task)CFRunLoopWakeUp(runLoop.getCFRunLoop())}}func stop() {thread?.cancel()CFRunLoopStop(runLoop?.getCFRunLoop())}}// 使用示例let permanentThread = PermanentThread()permanentThread.executeTask {print("在常驻线程中执行耗时任务")// 这里的代码在子线程执行}
注意:在现代 iOS 开发中,GCD 已经能很好地管理线程生命周期,除非有特殊需求(如需要精确控制线程生命周期、处理特定端口的回调),否则不建议手动保活线程。

5.2 延迟执行与取消
performSelector:withObject:afterDelay:的本质是创建一个 Timer 并添加到当前线程的 RunLoop。
// 延迟 2 秒执行perform(#selector(delayedMethod), with: nil, afterDelay: 2.0)// 取消延迟执行NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(delayedMethod), object: nil)
陷阱:如果当前线程没有开启RunLoop,afterDelay系列方法是无效的。
DispatchQueue.global().async {// 子线程默认没有 RunLoopself.perform(#selector(self.method), with: nil, afterDelay: 1.0)// 这个方法永远不会被调用!// 如果要生效,需要手动开启 RunLoopRunLoop.current.run()}

5.3 滑动时加载图片优化
在 UITableView 滑动时,不加载图片,等滑动停止后再加载,可以显著提升滑动流畅度。
// 利用 RunLoop 的 Mode 特性func scrollViewDidScroll(_ scrollView: UIScrollView) {// 滑动时不加载图片}func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {if !decelerate {// 滑动停止,开始加载可见区域的图片loadImagesForVisibleCells()}}func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {// 减速停止,开始加载可见区域的图片loadImagesForVisibleCells()}func loadImagesForVisibleCells() {// 获取可见 cell,加载图片}
更优雅的实现:利用CFRunLoopPerfo-rmBlock 在 RunLoop 空闲时执行任务。
// 在 RunLoop 即将休眠前执行低优先级任务CFRunLoopPerformBlock(CFRunLoopGetMain(), .default) {// 加载图片、预计算高度等耗时但不紧急的任务self.loadImagesForVisibleCells()}

5.4 AutoreleasePool 与 RunLoop
苹果在主线程 RunLoop 中注册了两个 Observer,用于管理自动释放池:
Entry Observer(优先级最高):在 RunLoop 进入时创建 AutoreleasePool。
BeforeWaiting Observer(优先级最低):在 RunLoop 休眠前销毁旧的 AutoreleasePool,并创建一个新的。
// 简化代码演示void setupAutoreleasePoolForRunLoop() {CFRunLoopObserverRef entryObserver = CFRunLoopObserverCreate(..., kCFRunLoopEntry, ...);CFRunLoopAddObserver(runloop, entryObserver, kCFRunLoopCommonModes);CFRunLoopObserverRef beforeWaitingObserver = CFRunLoopObserverCreate(..., kCFRunLoopBeforeWaiting, ...);CFRunLoopAddObserver(runloop, beforeWaitingObserver, kCFRunLoopCommonModes);}
因此,Autorelease 对象的释放时机是
RunLoop 即将休眠时。这也是为什么我们在 viewDidLoad 中创建的大量临时对象,不会立即释放,而是等到本次 RunLoop 循环结束。
六、RunLoop 调试与可视化工具

6.1 lldb 命令调试
在 Xcode 断点中,使用 lldb 命令查看 RunLoop 状态:
# 打印主线程 RunLoop(lldb) po [NSRunLoop mainRunLoop]# 打印当前 RunLoop 的 Mode(lldb) po [[NSRunLoop currentRunLoop] currentMode]# 查看 RunLoop 中的所有 Timer(lldb) po [[NSRunLoop currentRunLoop] timers]# 查看 RunLoop 中的所有 Source(lldb) po [[NSRunLoop currentRunLoop] sources]
输出示例:
<CFRunLoop 0x6000001f0300 [0x10a8c7c50]> {current mode = kCFRunLoopDefaultMode,common modes = {UITrackingRunLoopMode,kCFRunLoopDefaultMode},common mode items = {Source0 { ... },Source1 { ... },Timer { ... },Observer { ... }},modes = {UITrackingRunLoopMode { ... },GSEventReceiveRunLoopMode { ... },kCFRunLoopDefaultMode { ... },UIInitializationRunLoopMode { ... }}}

6.2 自定义 RunLoop 监控工具
我们可以写一个调试工具,实时在控制台输出 RunLoop 状态变化:
class RunLoopDebugger {static func start() {let observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault,CFRunLoopActivity.allActivities.rawValue,true,0) { observer, activity inlet mode = RunLoop.current.currentMode?.rawValue ?? "nil"var activityName = ""switch activity {case .entry: activityName = "Entry"case .beforeTimers: activityName = "BeforeTimers"case .beforeSources: activityName = "BeforeSources"case .beforeWaiting: activityName = "BeforeWaiting"case .afterWaiting: activityName = "AfterWaiting"case .exit: activityName = "Exit"default: activityName = "Unknown"}print("🔵 [RunLoop] 状态: \(activityName), Mode: \(mode)")}CFRunLoopAddObserver(CFRunLoopGetMain(), observer, .commonModes)}}// 在 AppDelegate 中调用RunLoopDebugger.start()
运行 App 后,控制台会实时输出类似信息:
🔵 [RunLoop] 状态: Entry, Mode: UIInitializationRunLoopMode🔵 [RunLoop] 状态: BeforeTimers, Mode: kCFRunLoopDefaultMode🔵 [RunLoop] 状态: BeforeSources, Mode: kCFRunLoopDefaultMode🔵 [RunLoop] 状态: BeforeWaiting, Mode: kCFRunLoopDefaultMode😴 即将休眠...⏰ 被唤醒,原因:触摸事件🔵 [RunLoop] 状态: AfterWaiting, Mode: kCFRunLoopDefaultMode🔵 [RunLoop] 状态: BeforeTimers, Mode: UITrackingRunLoopMode...

6.3 Instruments 中的 Time Profiler
使用Xcode Instruments的Time Profiler 工具,可以清晰地看到主线程 RunLoop 在不同状态下的耗时分布。在分析性能问题时,关注 CFRunLoopRun相关的调用栈,能快速定位卡顿原因。
七、常见面试题深度解析
Q1
为什么 UI 操作必须在主线程?
A
UIKit 框架本身不是线程安全的。但更深层次的原因是,主线程的 RunLoop 是整个事件分发系统的中枢。触摸事件、渲染回调、CAAnimation 的代理回调等,都依赖于主线程 RunLoop 的 Source0 和 Source1 机制。如果允许子线程操作 UI,就破坏了这套事件驱动模型的统一性,会导致不可预测的并发问题。
Q2
performSelector:afterDelay:在子线程能用吗?
A
能用,但前提是子线程必须开启了 RunLoop。afterDelay 的本质是创建一个 NSTimer 并添加到当前线程的 RunLoop。如果没有 RunLoop,Timer 无法被调度,方法就不会执行。
Q3
CADisplayLink和NSTimer有什么区别?
A
特性 | CADisplayLink | NSTimer |
驱动源 | 屏幕刷新信号(Vsync) | RunLoop 的 Timer 机制 |
精度 | 与屏幕刷新率同步(60/120 fps) | 受 RunLoop 忙碌程度影响,可能不准 |
用途 | 动画、绘制、视频渲染 | 一般定时任务 |
RunLoop Mode | 默认只在 DefaultMode 和 TrackingMode | 需手动指定 |
CADisplayLink 的特殊之处在于,它的回调是在 RunLoop 处理完所有事件之后、渲染之前执行的,因此适合做 UI 更新。
Q4
如何检测主线程卡顿?
A
核心思路是利用RunLoop Observer 监控 BeforeSources到BeforeWaiting之间的耗时。如果主线程在这段时间内耗时过长(比如超过 50ms),说明在处理某个任务时发生了阻塞。可以结合子线程的 Ping 机制来检测超时。
Q5
AFNetworking 2.x 为什么要常驻线程?
A
AFNetworking 2.x 使用NSURLConnect-tion进行网络请求。NSURLConnection 的代理回调必须在创建它的线程上执行,且该线程必须有 RunLoop。为了不让主线程处理网络回调(可能影响 UI),AFN 创建了一个常驻线程,并开启了 RunLoop,专门用来接收网络回调。AFNetworking3.x 改用NSURLSession后,不再需要常驻线程,因为NSURLSession 自己管理了一个后台线程池。
总结与进阶建议
RunLoop 是 iOS 开发进阶的必经之路。掌握了 RunLoop,你才能回答:
App 为什么能一直运行?
滑动时 Timer 为什么暂停?
自动释放池什么时候释放对象?
主线程卡顿怎么检测?
核心知识点回顾
层次 | 核心概念 | 关键要点 |
底层 | Mach Port | 内核消息传递,实现线程休眠与唤醒 |
中层 | Source / Timer / Observer | 事件源、定时器、状态监听者 |
上层 | Mode | 工作模式,决定 RunLoop 处理哪些事件 |
应用 | 主线程 RunLoop | 自动开启,负责 UI 事件分发 |
进阶学习路径
阅读源码:苹果开源了 CoreFoundation 框架,CFRunLoop.c是必读文件。
理解 GCD 与 RunLoop 的关系:GCD 的主队列是如何与 RunLoop 交互的?
研究事件响应链:触摸事件是如何从 Source0 传递到 hitTest再到UIResponder 的?
探索渲染流程:CALayer的渲染与 RunLoop 的 BeforeWaiting 时机有何关联?
文字来源 | iOS组
编辑排版 | 黄佩婷
新思路出品

夜雨聆风