iOS 开发中的 RunLoop

由浅入深,从基本概念到源码解析,带你全面理解 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 SourcesTimer 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
// 获取当前线程的 RunLoop
let runLoop = RunLoop.current

// 获取主线程 RunLoop(任意线程可调用)
let mainRunLoop = RunLoop.main

2.2 主线程 vs 子线程

  • 主线程:应用启动时,主线程 RunLoop 自动创建并运行
  • 子线程:默认不启动 RunLoop,需要主动调用 run 才会进入事件循环
1
2
3
4
5
6
7
// 主线程 RunLoop 自动运行,无需手动启动
// 子线程 RunLoop 默认不运行
Thread.detachNewThread {
let runLoop = RunLoop.current
runLoop.add(Port(), forMode: .default) // 必须有 source,否则 run 会立即退出
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」。常见实现如 NSTimerCADisplayLink

3.3 Run Loop Observer(观察者)

Observer 不处理事件,而是 观察 RunLoop 的状态变化,可监控以下活动:

1
2
3
4
5
6
7
8
9
// CFRunLoopActivity 定义
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入 Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出 Loop
}

四、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 默认包含 NSDefaultRunLoopModeUITrackingRunLoopMode。把 Timer/Source 加到 CommonModes,会在上述两种模式切换时都得到回调。

4.3 常见问题:Timer 滑动时失效

1
2
3
4
5
6
7
8
// 仅加入 DefaultMode:滑动 UITableView 时 Timer 暂停
let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
print("tick")
}
RunLoop.main.add(timer, forMode: .default)

// 加入 CommonModes:滑动时 Timer 继续触发
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; // Mode 名称,如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Source0 集合
CFMutableSetRef _sources1; // Source1 集合
CFMutableArrayRef _observers; // Observer 数组
CFMutableArrayRef _timers; // Timer 数组
...
};

struct __CFRunLoop {
CFMutableSetRef _commonModes; // 标记为 Common 的 Mode 集合
CFMutableSetRef _commonModeItems; // 加到 Common 的 Source/Timer/Observer
CFRunLoopModeRef _currentMode; // 当前 Mode
CFMutableSetRef _modes; // 所有 Mode
...
};

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; // Mode 为空则直接返回

// 1. 通知 Observers:即将进入 Loop
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);

__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
do {
// 2. 通知 Observers:即将处理 Timer
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);

// 3. 通知 Observers:即将处理 Source0
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);

// 4. 执行被加入的 Block
__CFRunLoopDoBlocks(runloop, currentMode);

// 5. 处理 Source0
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
__CFRunLoopDoBlocks(runloop, currentMode);

// 6. 若有 Source1 就绪,处理 Source1 消息
if (__Source0DidDispatchPortLastTime) {
if (__CFRunLoopServiceMachPort(dispatchPort, &msg))
goto handle_msg;
}

// 7. 通知 Observers:即将进入休眠
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}

// 8. 调用 mach_msg 等待消息,线程休眠,直到被唤醒
__CFRunLoopServiceMachPort(waitSet, &msg, ...);
// 唤醒条件:Port 事件、Timer 到期、超时、手动唤醒

// 9. 通知 Observers:刚刚被唤醒
__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); // GCD 主队列
} else {
__CFRunLoopDoSource1(runloop, currentMode, source1, msg); // Source1
}

__CFRunLoopDoBlocks(runloop, currentMode);

} while (retVal == 0); // 根据 retVal 决定是否继续循环
}

// 10. 通知 Observers:即将退出 Loop
__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 事件响应链路

  1. 硬件事件(触摸、按键等)→ IOKit 生成 IOHIDEvent
  2. SpringBoard 接收,通过 Mach Port 转发给 App
  3. 主线程 RunLoop 的 Source1 回调 __IOHIDEventSystemClientQueueCallback
  4. 内部调用 _UIApplicationHandleEventQueue() 包装成 UIEvent
  5. 经 hitTest、响应链,最终到达对应 Target-Action 或 touches 方法

6.3 手势识别

  • _UIApplicationHandleEventQueue() 识别到手势时,会打断 touches 序列
  • 将 UIGestureRecognizer 标记为待处理
  • kCFRunLoopBeforeWaiting 的 Observer _UIGestureRecognizerUpdateObserver 中统一执行手势回调

6.4 界面更新

  • 调用 setNeedsLayout / setNeedsDisplay 后,视图被标记为待更新
  • kCFRunLoopBeforeWaitingkCFRunLoopExit 的 Observer 中,Core Animation 执行实际布局和绘制

6.5 PerformSelector

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) // 添加 Port 作为 Source,否则 run 会立即退出
runLoop.run()
}
}
}
1
2
3
4
5
6
7
8
9
// AFNetworking 2.x 的经典实现
+ (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
// 仅在 Default 模式执行
RunLoop.current.perform(#selector(doWork), target: self, argument: nil, order: 0, modes: [.default])

// 使用 CFRunLoop 添加 Block(iOS 10+)
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
// scheduledTimer 会自动加入当前 RunLoop 的 Default 模式
let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
print("tick")
}

// 若在子线程使用,需确保 RunLoop 在运行
DispatchQueue.global().async {
let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
print("tick")
}
RunLoop.current.add(timer, forMode: .default)
RunLoop.current.run() // 必须 run,否则 Timer 不会触发
}

八、常见面试要点

  1. RunLoop 和线程的关系:一一对应,主线程自动运行,子线程需手动启动。
  2. Source0 与 Source1:Source0 需手动标记;Source1 基于 Port,可主动唤醒。
  3. Mode 的作用:隔离不同场景的事件,滑动时切换到 UITrackingRunLoopMode 保证流畅。
  4. CommonModes:把 Timer/Source 加到 CommonModes 可在 Default 和 Tracking 下都收到回调。
  5. AutoreleasePool 与 RunLoop:主线程在 Entry、BeforeWaiting、Exit 时自动 Push/Pop。
  6. 卡顿监控思路:用 CFRunLoopObserver 监听主线程,结合超时判定卡顿。

九、参考

Author

Felix Tao

Posted on

2019-10-02

Updated on

2020-09-22

Licensed under