由浅入深,从基本概念到源码解析,带你全面理解 iOS 事件循环机制
一、什么是 RunLoop?
1.1 从 Event Loop 说起
通常,一个线程一次只能执行一个任务,任务完成后线程就会退出。但在 GUI 应用中,我们需要一种机制:线程能够随时处理事件或消息,并且在空闲时不会退出。这种机制称为 Event Loop(事件循环)。
1 2 3 4 5 6 7
| function main initialize() while message != quit message := get_next_message() process_message(message) end while end function
|
Event Loop 的核心问题是:
- 如何管理事件/消息
- 如何让线程在没有任务时休眠,避免 CPU 空转
- 如何在事件到来时唤醒线程处理
1.2 RunLoop 是什么?
RunLoop 是苹果在 OSX/iOS 上对 Event Loop 的实现。它是一个对象,负责:
- 管理需要处理的事件/消息
- 提供入口函数执行「接收消息 → 等待 → 处理」的循环
- 在没有事件时让线程休眠,有事件时唤醒并分发处理
RunLoop 从 Input Sources 和 Timer Sources 接收事件,然后在线程中执行对应的 Handler。
1.3 NSRunLoop 与 CFRunLoop
苹果提供了两层 API:
| 类型 |
说明 |
线程安全 |
| NSRunLoop |
基于 CFRunLoop 的 OC 封装,面向对象 API |
否 |
| CFRunLoopRef |
CoreFoundation 的 C 实现 |
是 |
日常开发多用 NSRunLoop,底层和性能相关则直接用 CFRunLoop。
二、RunLoop 与线程
2.1 一一对应关系
- RunLoop 和线程是 一一对应 的
- 每个线程(含主线程)都有唯一的 RunLoop
- RunLoop 在 首次获取 时创建,线程结束时 销毁
- 只能在 对应线程内部 获取该线程的 RunLoop(主线程除外)
1 2 3 4 5
| let runLoop = RunLoop.current
let mainRunLoop = RunLoop.main
|
2.2 主线程 vs 子线程
- 主线程:应用启动时,主线程 RunLoop 自动创建并运行
- 子线程:默认不启动 RunLoop,需要主动调用
run 才会进入事件循环
1 2 3 4 5 6 7
|
Thread.detachNewThread { let runLoop = RunLoop.current runLoop.add(Port(), forMode: .default) runLoop.run() }
|
三、核心组件
3.1 Run Loop Source(事件源)
RunLoop 从两类 Source 接收事件:
| 类型 |
名称 |
特点 |
典型场景 |
| Source0 |
Custom Input Source |
需手动标记为待处理,不主动唤醒线程 |
UIEvent、CFSocket、普通回调 |
| Source1 |
Port-Based Source |
基于 Mach Port,可主动唤醒 RunLoop |
系统触摸事件、进程间通信 |
1 2
| Source0:用户事件、自定义事件 → 需外部调用 CFRunLoopSourceSignal 标记 Source1:系统 Port 事件 → 内核可主动唤醒线程
|
3.2 Run Loop Timer(定时器)
Timer 本质是 基于 Port 的 Source,所有 Timer 共用同一个「Mode Timer Port」。常见实现如 NSTimer、CADisplayLink。
3.3 Run Loop Observer(观察者)
Observer 不处理事件,而是 观察 RunLoop 的状态变化,可监控以下活动:
1 2 3 4 5 6 7 8 9
| typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = (1UL << 0), kCFRunLoopBeforeTimers = (1UL << 1), kCFRunLoopBeforeSources = (1UL << 2), kCFRunLoopBeforeWaiting = (1UL << 5), kCFRunLoopAfterWaiting = (1UL << 6), kCFRunLoopExit = (1UL << 7), }
|
四、Run Loop Mode
4.1 什么是 Mode?
Mode 是 RunLoop 的「工作模式」。一个 RunLoop 包含多个 Mode,每个 Mode 下有各自的 Source、Timer、Observer。同一时刻 RunLoop 只运行在一个 Mode 下,只处理该 Mode 里的 Source/Timer/Observer。
这样可以把不同场景的事件隔离,例如:
- 默认模式下处理普通事件
- 滑动 ScrollView 时切到
UITrackingRunLoopMode,只处理触摸,保证滑动流畅
4.2 常见 Mode
| Mode |
说明 |
| NSDefaultRunLoopMode |
默认模式,App 空闲时主线程通常在此模式 |
| UITrackingRunLoopMode |
界面追踪模式,ScrollView 滑动时切换到此 |
| NSRunLoopCommonModes |
占位符,表示「Common 模式集合」 |
NSRunLoopCommonModes 默认包含 NSDefaultRunLoopMode 和 UITrackingRunLoopMode。把 Timer/Source 加到 CommonModes,会在上述两种模式切换时都得到回调。
4.3 常见问题:Timer 滑动时失效
1 2 3 4 5 6 7 8
| let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in print("tick") } RunLoop.main.add(timer, forMode: .default)
RunLoop.main.add(timer, forMode: .common)
|
4.4 数据结构示意
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| struct __CFRunLoopMode { CFStringRef _name; CFMutableSetRef _sources0; CFMutableSetRef _sources1; CFMutableArrayRef _observers; CFMutableArrayRef _timers; ... };
struct __CFRunLoop { CFMutableSetRef _commonModes; CFMutableSetRef _commonModeItems; CFRunLoopModeRef _currentMode; CFMutableSetRef _modes; ... };
|
commonModeItems 会被自动同步到所有 Common Mode 中,这就是把 Timer 加到 .common 能解决滑动暂停的原因。
五、RunLoop 工作流程(源码级)
5.1 整体流程
CFRunLoopRun 的简化调用链:
1 2 3
| void CFRunLoopRun(void) { CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false); }
|
核心逻辑在 CFRunLoopRunSpecific 里,内部是一个 do-while 循环,大致步骤如下:
5.2 核心源码流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) { CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false); if (__CFRunLoopModeIsEmpty(currentMode)) return;
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) { do { __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
__CFRunLoopDoBlocks(runloop, currentMode);
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle); __CFRunLoopDoBlocks(runloop, currentMode);
if (__Source0DidDispatchPortLastTime) { if (__CFRunLoopServiceMachPort(dispatchPort, &msg)) goto handle_msg; }
if (!sourceHandledThisLoop) { __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting); }
__CFRunLoopServiceMachPort(waitSet, &msg, ...);
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
handle_msg: if (msg_is_timer) { __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time()); } else if (msg_is_dispatch) { __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg); } else { __CFRunLoopDoSource1(runloop, currentMode, source1, msg); }
__CFRunLoopDoBlocks(runloop, currentMode);
} while (retVal == 0); }
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); }
|
5.3 流程简图
1 2 3 4 5 6
| Entry → BeforeTimers → BeforeSources → DoBlocks → DoSources0 ↓ Exit ← BeforeExit ← AfterWaiting ← mach_msg 等待 ↑ ↓ └──────── handle_msg ───────┘ (Timer / Source1 / Dispatch)
|
六、基于 RunLoop 的系统功能
6.1 AutoreleasePool
主线程 RunLoop 中注册了两个 Observer,回调都是 _wrapRunLoopWithAutoreleasePoolHandler:
| 时机 |
回调 |
作用 |
| kCFRunLoopEntry |
_objc_autoreleasePoolPush() |
创建自动释放池 |
| kCFRunLoopBeforeWaiting |
_objc_autoreleasePoolPop() + Push |
释放旧池并创建新池 |
| kCFRunLoopExit |
_objc_autoreleasePoolPop() |
退出时释放池 |
因此主线程上的事件回调、Timer 回调等都会在 AutoreleasePool 包裹下执行,一般无需手动创建。
6.2 事件响应链路
- 硬件事件(触摸、按键等)→ IOKit 生成 IOHIDEvent
- SpringBoard 接收,通过 Mach Port 转发给 App
- 主线程 RunLoop 的 Source1 回调
__IOHIDEventSystemClientQueueCallback
- 内部调用
_UIApplicationHandleEventQueue() 包装成 UIEvent
- 经 hitTest、响应链,最终到达对应 Target-Action 或 touches 方法
6.3 手势识别
_UIApplicationHandleEventQueue() 识别到手势时,会打断 touches 序列
- 将 UIGestureRecognizer 标记为待处理
- 在 kCFRunLoopBeforeWaiting 的 Observer
_UIGestureRecognizerUpdateObserver 中统一执行手势回调
6.4 界面更新
- 调用
setNeedsLayout / setNeedsDisplay 后,视图被标记为待更新
- 在 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 的 Observer 中,Core Animation 执行实际布局和绘制
performSelector:afterDelay: 和 performSelector:onThread: 内部都会创建 Timer 并加入对应线程的 RunLoop。若该线程没有 RunLoop 或 RunLoop 未运行,这些方法不会生效。
七、实战示例
7.1 常驻线程(如 AFNetworking)
子线程默认不跑 RunLoop,需要手动添加 Source 并 run:
1 2 3 4 5 6 7 8 9 10
| class NetworkThread: Thread { override func main() { autoreleasepool { self.name = "AFNetworking" let runLoop = RunLoop.current runLoop.add(Port(), forMode: .default) runLoop.run() } } }
|
1 2 3 4 5 6 7 8 9
| + (void)networkRequestThreadEntryPoint:(id)__unused object { @autoreleasepool { [[NSThread currentThread] setName:@"AFNetworking"]; NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; [runLoop run]; } }
|
7.2 使用 Observer 监控卡顿
通过观察主线程 RunLoop 状态,检测某个阶段耗时是否过长:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| import UIKit
class RunLoopMonitor { private var observer: CFRunLoopObserver? private var semaphore: DispatchSemaphore private var timeout = true private let timeoutCount: Int = 5 private var count = 0
init() { semaphore = DispatchSemaphore(value: 0) }
func start() { let activities: CFRunLoopActivity = [.beforeWaiting, .afterWaiting] var context = CFRunLoopObserverContext( version: 0, info: Unmanaged.passUnretained(self).toOpaque(), retain: nil, release: nil, copyDescription: nil )
observer = CFRunLoopObserverCreate( kCFAllocatorDefault, activities.rawValue, true, 0, { _, activity, info in guard let info = info else { return } let monitor = Unmanaged<RunLoopMonitor>.fromOpaque(info).takeUnretainedValue() monitor.handleObserverCallback(activity) }, &context )
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, .commonModes)
DispatchQueue.global().async { [weak self] in self?.monitor() } }
private func handleObserverCallback(_ activity: CFRunLoopActivity) { if activity == .beforeWaiting || activity == .afterWaiting { count = 0 timeout = false } semaphore.signal() }
private func monitor() { while true { let result = semaphore.wait(timeout: .now() + 1) if result == .timedOut { if timeout { count += 1 if count >= timeoutCount { print("⚠️ 主线程可能发生卡顿") } } else { timeout = true count = 0 } } } } }
|
7.3 在指定 RunLoop 模式执行任务
1 2 3 4 5 6 7 8
| RunLoop.current.perform(#selector(doWork), target: self, argument: nil, order: 0, modes: [.default])
CFRunLoopPerformBlock(CFRunLoopGetMain(), CFRunLoopMode.defaultMode.rawValue as CFString) { print("在 RunLoop 当前迭代中执行") } CFRunLoopWakeUp(CFRunLoopGetMain())
|
7.4 NSTimer 与 RunLoop
1 2 3 4 5 6 7 8 9 10 11 12 13
| let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in print("tick") }
DispatchQueue.global().async { let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in print("tick") } RunLoop.current.add(timer, forMode: .default) RunLoop.current.run() }
|
八、常见面试要点
- RunLoop 和线程的关系:一一对应,主线程自动运行,子线程需手动启动。
- Source0 与 Source1:Source0 需手动标记;Source1 基于 Port,可主动唤醒。
- Mode 的作用:隔离不同场景的事件,滑动时切换到
UITrackingRunLoopMode 保证流畅。
- CommonModes:把 Timer/Source 加到 CommonModes 可在 Default 和 Tracking 下都收到回调。
- AutoreleasePool 与 RunLoop:主线程在 Entry、BeforeWaiting、Exit 时自动 Push/Pop。
- 卡顿监控思路:用 CFRunLoopObserver 监听主线程,结合超时判定卡顿。
九、参考