iOS - RunLoop 相关知识点
为什么要有
RunLoop?
- 背景:线程执行完任务就会退出,但主线程(或者一些后台线程等)我们希望它能够一直存在、持续等待事件(触摸、定时器、网络回调等)。
原始的解决方案:
- 如果写成
while(1) {}类似的死循环,会出现问题:- 线程会持续占用 CPU(忙等待),浪费资源。
🍎给出的答案:
- 👉 RunLoop —— 让线程在有事件时被唤醒执行,没有事件时进入休眠。
- 在没有 RunLoop 的情况下,线程要么退出、要么忙等,无法高效等待事件。
- Apple 的设计是 —— 让线程进入一个由内核管理的 事件循环系统,当有事件发生时唤醒线程,没有事件时进入休眠。
- 这就是 RunLoop 的核心思想:事件驱动 + 内核唤醒 + 自动调度。
核心概念
本质:runloop本质是一个基于 事件驱动 的循环机制,底层依托内核的 mach port 进行等待和消息分发。用于 调度 定时器、输入源和观察者,让线程在“有事活跃、无事休眠”之间高效切换。
核心功能:
- 保持程序的持续运行(长连接、后台任务等)
- 监听 App 中的各种事件(触摸、滑动、定时器事件等)
- 自动管理
AutoreleasePool - 控制线程的状态,节省 CPU 资源,提高程序性能
好处:相较于 线程轮询,runloop的工作机制效率更高。它允许在闲置时将线程置于睡眠状态,CPU进入低功耗状态,从而节约能源资源。
地位:在 iOS 中,RunLoop 是整个系统事件机制的核心支柱。主线程的 RunLoop 是 UIKit 事件响应、定时器、动画、触摸、AutoreleasePool、GCD 主队列 的共同基础。
组成角色

/// CFRunLoop 核心成员
struct __CFRunLoop {
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
mach_port_t _wakeUpPort; // 唤醒 RunLoop 的系统端口
};
一、输入源 (Sources)
- Source0(非端口事件): 用户态事件源,App内部事件,不能自动唤醒 runloop,需要手动 signal。
- 特点:
- 不依赖系统内核或端口。
- RunLoop 不会被系统自动唤醒,必须手动调用 signal + wakeup
- 常用于线程间通信 或 自定义事件调度
- 例:
performSelector: onThread::- 系统把
selector封装事件为 source0。 - 加入到目标线程 runloop 的 source0 队列。
- 内部调用唤醒 signal + wakeup 唤醒 runloop。
- 如果目标线程 runloop 正在执行,依然等待到下一轮循环的 source0 阶段再执行。
- 系统把
- 特点:
- Source1(端口事件): 内核级事件源,由RunLoop和内核管理,Mach port驱动。
- 特点:
- 依赖系统的端口(mach port),用于接收内核或者其他线程发送的消息。
- 用于系统级事件(触摸、网络)或者跨线程通信(
CFMessagePort、CFMachPort)。 - 当有端口消息到达时,内核通过
mach_msg唤醒 RunLoop 所在线程。
- 例:
- UI事件:触摸、点击(基于 mach port 通知主线程 runloop)
- 网络事件:Socket 数据到达 → 内核通知 → Source1 被唤醒 → 调用回调处理数据。
- Port 消息:
NSMachPort、CFMessagePort等。
- 特点:
二、定时器 (Timers)
独立于 source 的一种事件类型。
- 机制:
- runloop 维护的一组定时器(
NSTimer/performSelector:afterDelay:)。 - 每轮循环时检查定时器的 “下一次触发时间”。
- 如果到达了触发点,执行对应回调。
- 若 runloop 处于休眠状态,内核会在最近一个 Timer 到期时唤醒线程。
- runloop 维护的一组定时器(
- 注意:
- Timer 属于 runloop 的事件调度机制,不依赖 mach port 消息。
三、观察者 (Observers)

监听 RunLoop 状态变化的钩子(CFRunLoopObserver)
- 用途:
- 监听 runloop 各个阶段(Entry/BeforeTimers/BeforeSources/BeforeWaiting/AfterWaiting/Exit)。
- 常用于:
- 性能调试(卡顿监测)
- 帧率刷新(
CADisplayLink内部机制) - 自动管理 AutoreleasePool(系统就在此阶段创建/销毁池子)
四、模式 (Modes)

每个 RunLoop 必须且只能运行在一个 Mode 下(Mode 可切换)。
每个 Mode 定义了 RunLoop 本轮循环中要监听哪些事件源(Sources、Timers、Observers)。
为什么需要 Mode ?
- 因为不同场景下需要“隔离事件源”。
- 比如用户滚动时,RunLoop 会切换到 tracking 模式以屏蔽 default 模式下的定时器。
常用 Mode
| 标题 | 描述 | 应用场景 |
|---|---|---|
| NSDefaultRunLoopMode | 默认模式 | 普通任务、定时器 |
| UITrackingRunLoopMode | UI追踪模式 | 滚动、拖拽时系统使用 |
| NSRunLoopCommonModes | 一种“标记集合”(非真正的Mode) | 用来把 Source/Timer 同时加入多个 Mode |
| GSEventReceiveRunLoopMode | 系统底层模式 | 接收系统级输入事件(触摸、键盘) |
| kCFRunLoopCommonModes | Core Foundation 层关键字 | 与 NSRunLoopCommonModes 一致 |
-
NSDefaultRunLoopMode
- 主线程默认运行在该模式下。
- 通过
NSTimer、performSelector:afterDelay:创建的定时器都会加入这个 Mode。 - 缺点:当用户滚动
UIScrollView时,RunLoop 会自动切换到 UITrackingRunLoopMode,导致 Default 模式下的 Timer 暂停。 - 例子:
- Source0:
performSelector:onThread: - Timer:
NSTimer、afterDelay: - Observer: AutoreleasePool create/drain(注册在 CommonModes 下)
- Source0:
-
UITrackingRunLoopMode
- 用户触摸、滚动、拖动页面时,runloop 会临时切换到 tracking 模式。
- 滚动结束后,runloop 会自动切回 Default 模式。
- 例子:
- Source1: 触摸 / 滑动事件(基于 Mach port)
- Observer: AutoReleasePool create/drain(注册在 CommonModes 下)
-
NSRunLoopCommonModes
-
不是 Mode,而是一个“模式集合标签”
-
任何被加入到 CommonModes 的 Source/Timer,会同时参与多个 Mode。
-
例子:
- 让某些
NSTimer同时参与 default + tracking。
NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) { NSLog(@"Tick"); }]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; - 让某些
-
-
其他内部 Mode
- UIInitializationRunLoopMode: App 启动阶段使用(私有)。
- GSEventReceiveRunLoopMode:
CoreGraphics层,用于接收系统级输入事件(触摸、键盘)。 - kCFRunLoopCommonModes:CFRunLoop 层关键字,等价于 NSRunLoopCommonModes。
模式之间的关系与区别
从上述文章看来,UITrackingRunLoopMode 和 GSEventReceiveRunLoopMode 都参与了 UI 事件处理,那他们是否冲突呢?
| 模式 | 所属层级 | 作用 | 谁在用? |
|---|---|---|---|
| GSEventReceiveRunLoopMode | CoreGraphics/CoreAnimation 层 | 接收来自内核(Mach Port)的原始触摸、键盘事件 | 系统私有 |
| UITrackingRunLoopMode | UIKit 层 | 当用户开始滚动/拖拽时,UIKit 临时切换到此模式来追踪触摸事件 | UIKit 控制的住 runloop |
[ 内核 mach_msg ]
↓
[ CoreGraphics 接收原始触摸事件 ] (GSEventReceiveRunLoopMode)
↓
[ UIKit 处理触摸与滚动逻辑 ](UITrackingRunLoopMode)
↓
[ App 代码响应事件 ]
从他们的分工与所属不难看出,他们并不冲突,而是事件分发链路的上下层关系。
GSEventReceive 在底层接收输入,UITracking 在上层进行滚动和触摸追踪。
RunLoop 的工作原理

线程与 RunLoop
| 特性 | 主线程 runloop |
子线程 runloop |
|---|---|---|
| 自动创建 | ✅ | ❌ |
| 自动启动 | ✅ | ❌需要手动启动 |
| 自动管理 Pool | ✅ | ❌ |
| 生命周期 | app退出 | 手动退出、线程结束 |
| 常见用途 | UI事件、主队列任务、动画刷新 | 后台任务、定时器、网络回调、自定义输入源 |
- 子线程为什么 Timer 不会触法?
答:子线程没有 runloop 或没有进入循环。timer 依赖于 runloop,需要手动启动。
- 为什么 runloop 让线程“有事活跃,无事休眠”?
答:sleep 阶段阻塞在内核消息等待上,不消耗 CPU。
AutoreleasePool 与 RunLoop
AutoreleasePool:内存回收的延迟释放机制
RunLoop:基于事件驱动的循环机制
总体关系:RunLoop 在每一轮循环中自动创建和销毁 AutoreleasePool,以此用来管理这一轮产生的 autorelease 对象,确保在循环结束时被及时释放。
线程与 AutoreleasePool 栈
- 每个线程都有自己的 AutoreleasePool 栈结构(由 runtime 维护),哪怕没有显示写
@autoreleasepool。 - 但是线程启动时栈是空的,只有在执行 autorelease 操作时,系统才会 懒加载创建池页(AutoreleasePoolPage) 来接收对象。
主线程的行为
- 主线程拥有默认的 RunLoop(由系统在
UIApplicationMain启动时创建)。 - 主线程 RunLoop 由 UIKit 注册并托管,含有 pool observer,自动在每轮循环创建/销毁 AutoreleasePool。
| RunLoop 阶段 | 行为 |
|---|---|
| Entry | 创建新的 AutoreleasePool |
| BeforeWaiting | 销毁旧 Pool 并新建一个空 Pool(防止长时间未释放) |
| Exit | 销毁当前 Pool |
- 因此,主线程在每一轮事件循环结束时,都会清理掉该轮产生的临时对象。
子线程的行为
-
子线程默认 没有 RunLoop,系统也不会自动为它管理 AutoreleasePool。
- 子线程 RunLoop 默认也没有 pool observer,不会自动管理 autoreleasepool,,因此必须在启动前包一层 @autoreleasepool,否则 RunLoop 循环期间 autorelease 对象不会及时释放, 直到线程退出才清空,可能引发内存峰值或泄漏。
- (void)startBackgroundThread { NSThread *thread = [[NSThread alloc] initWithBlock:^{ @autoreleasepool { [[NSRunLoop currentRunLoop] run]; } }]; [thread start]; } -
但是子线程仍有 Pool 栈结构,只是如果不手动创建 Pool,也没有启动 RunLoop,autorelease 对象会延迟到线程结束才被释放。
- 若希望子线程能够周期性释放对象,有 2 种做法:
- 手动添加
@autoreleasepool; - 启动该线程的 RunLoop,让系统周期性创建和销毁 Pool。
- 手动添加
- 若希望子线程能够周期性释放对象,有 2 种做法:
GCD 与 RunLoop
GCD:线程的“任务调度机制”
RunLoop:线程的“事件循环机制”
总体关系:GCD 负责把任务派发到线程,RunLoop 负责让线程保活,并在空闲时处理事件,两者是协同关系。
GCD 与 RunLoop 的底层交互机制
GCD 和 RunLoop 的通信,是通过 Mach port 完成的。
- 当调用
dispatch_async(dispatch_get_main_queue(), block); - 系统把 block 放入主队列;
- 发送 Mach 消息,通知主线程的 RunLoop;
- RunLoop 被唤醒;
- RunLoop 调用
_dispatch_main_queue_callback_4CF(); - 执行队列里的 GCD block。
所以 RunLoop 驱动 GCD 主队列的执行,没有 RunLoop,主线程就不会被唤醒,主队列的任务也不会被执行。
主线程上的关系
- 主线程的 RunLoop 是由系统自动创建并运行的(在 UIApplicationMain 内部)。
- 同时,主线程上也有 GCD 主队列(Main Queue)。

当你向主队列派任务时,任务不会立即执行,而是由系统通过 RunLoop 的唤醒机制唤醒主线程,然后在 RunLoop 的 AfterWaiting 阶段执行这些主队列任务。
简言之: 主线程的 GCD 主队列,是通过 RunLoop 驱动执行的。
/// 二者关系:
[ GCD Main Queue ]
↓
[ dispatch_async(dispatch_get_main_queue(), ^{ ... }); ]
↓
[ 主线程执行任务(依靠 RunLoop 唤醒)]
子线程上的关系
- 子线程默认没有 RunLoop,因此 GCD 线程池 中的 线程 在执行完任务后就会销毁;
- 如果想让子线程 “常驻”,就要显示启动 RunLoop,然后 GCD 派发的任务才能在子线程上 持续被处理。

浙公网安备 33010602011771号