Fork me on GitHub

Run Loop 原理详解

Event Loop

通常,一个线程一次只能执行一个任务,任务完成后线程就会退出。但是在很多系统或框架中,需要实现一种这样机制:线程能够随时处理事件或消息,并且不会在执行完成后退出。这种机制称为 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 在很多系统或框架中都有对应的实现,如 Node.js 的事件处理,Windows 程序的消息循环,OSX/iOS 中的 RunLoop。实现这种机制的关键在于:如何管理事件/消息,如何让线程在没有处理任务时休眠以避免资源占用,如何在事件/消息到来时唤醒。

Run Loop

Run Loop 是 OSX/iOS 平台下对 Event Loop 机制的一种实现。当没有事件/消息时,Run Loop 进入休眠状态。当有事件/消息时,Run Loop 调用对应的 Handler 进行处理。如下图所示为 Run Loop 的工作模式示意图。

由上图可知,Run Loop 从 Input sourcesTimer sources 接收事件,然后在线程中处理。

Run Loop 本质上就是一个对象,其管理需要处理的事件/消息,并提供一个入口函数来执行上面 Event Loop 的逻辑。线程执行该函数后,会一直处于其内部的“接受消息->等待->处理”循环中,直到循环结束(比如传入 quit 的消息),函数返回。

OSX/iOS 中提供了两种 Run Loop 的实现:

  • CFRunLoopRef:CoreFoundation 框架对于 Run Loop 的实现,其提供纯 C 函数的 API(线程安全)。
  • NSRunLoop:基于 CFRunLoopRef 的封装,其提供面向对象的 API(非线程安全)。

Run Loop 与线程

Run Loop 和线程是一一对应的。每个线程(包括主线程)都有一个对应的 Run Loop 对象。Run Loop 对象的创建发生在第一次获取时(如果不主动获取,它一直都不会被创建);Run Loop 对象的销毁发生在线程结束时。

用户无法创建 Run Loop 对象,但可以获取系统提供的 Run Loop 对象。注意:只能在一个线程的内部获取其 Run Loop 对象,主线程不受限制

关于 Run Loop 的启动,主线程的 Run Loop 在应用启动时自动启动,其他线程的 Run Loop 默认不会自动启动,需手动启动。

Run Loop Source

从上面 Run Loop 工作模式示意图中可知,Run Loop 有两种接收事件的渠道:Input Source、Timer Source。

Input Source

Input Source 可分为三类:

  • Custom Input SourcesSource0):用户自定义的事件,不会主动触发事件,也不会主动唤醒 Run Loop 的线程。
  • Port-Based SourcesSource1):系统底层的 Port 事件(Mac Port),例如 CFSocketRef。一般用于通过内核和其他线程相互发送消息,应用层很少使用。这种 Source 可以主动唤醒 Run Loop 的线程。

Timer Source

Timer Source 即定时器事件。

Run Loop Observer

Run Loop 通过监控 Source 来决定是否执行处理程序。而 Runloop Observer 则监控 Runloop 本身的状态。 Runloop Observer 可监控的 runloop 事件如下所示:

1
2
3
4
5
6
7
8
9
/* Run Loop Observer Activities */
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

Run Loop Mode 即 Run Loop 工作模式。苹果文档中定义了 5 种 Mode:

  • NSDefaultRunLoopMode
  • NSConnectionReplyMode
  • NSModalPanelRunLoopMode
  • NSEventTrackingRunLoopMode
  • NSRunLoopCommonModes

iOS 中公开暴露出来的只有 NSDefaultRunLoopModeNSRunLoopCommonModesNSRunLoopCommonModes 实际上是一个 Mode 的集合,默认包括 NSDefaultRunLoopModeNSEventTrackingRunLoopMode

一个 Run Loop 可以包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。Run Loop 在某个时刻只能工作在一个 Mode 下,处理该 Mode 中的 Source/Timer/Observer。如果需要切换 Mode,只能退出 Run Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,使其互不影响。

如下所示为 Run Loop Mode 和 Run Loop 的部分源码定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};

typedef struct __CFRunLoop * CFRunLoopRef;

struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
...
};

Source/Timer/Observer 被统称为 Mode Item,一个 Item 可以被同时加入多个 Mode。但一个 Item 被重复加入同一个 Mode 时是不会有效果的。如果一个 Mode 中一个 Item 都没有,则 Run Loop 会直接退出,不进入“接受消息->等待->处理”循环。

Common Mode

一个 Mode 可以将自己标记为 Common(通过将其 Mode Name 添加到 Run Loop 的 _commonModes 中)。每当 Run Loop 内容发生变化,Run Loop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 Common 标记的所有 Mode 里

举例

主线程的 Run Loop 里有两个预置的 Mode:kCFRunLoopDefaultModeUITrackingRunLoopMode。这两个 Mode 都已经被标记为 CommonkCFRunLoopDefaultMode 是 App 平时所处的状态,UITrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。当创建一个 Timer 并加到 kCFRunLoopDefaultMode 时,Timer 会得到重复回调,但此时滑动 TableView 时,Run Loop 会将 Mode 切换为 UITrackingRunLoopMode。这是系统为了保持滑动流畅而做出的 Mode 切换。但这会导致 Timer 不被回调。

为了让 Timer 能在这两种 Mode 下都能得到回调,有 3 种解决方案:

  1. 将 Timer 分别加入两种 Mode
  2. 将 Timer 加入 Run Loop 的 _commonModeItems 中。因为,Run Loop 发生变化时,Run Loop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 Common 标记的所有 Mode 里。
  3. 在另一个线程执行和处理 Timer 事件,然后在主线程更新 UI。

Run Loop 工作流程

下图所示为 Run Loop 工作流程示意图。

如下所示为 Run Loop 工作流程的核心代码整理。

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/// RunLoop的实现
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {

/// 首先根据modeName找到对应mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
/// 如果mode里没有source/timer/observer, 直接返回。
if (__CFRunLoopModeIsEmpty(currentMode)) return;

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

/// 内部函数,进入loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {

Boolean sourceHandledThisLoop = NO;
int retVal = 0;
do {

/// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);

/// 4. RunLoop 触发 Source0 (非port) 回调。
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);

/// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
if (hasMsg) goto handle_msg;
}

/// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}

/// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
/// ? 一个基于 port 的Source 的事件。
/// ? 一个 Timer 到时间了
/// ? RunLoop 自身的超时时间到了
/// ? 被其他什么调用者手动唤醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
}

/// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);

/// 收到消息,处理消息。
handle_msg:

/// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}

/// 9.2 如果有dispatch到main_queue的block,执行block。
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}

/// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
else {
CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
if (sourceHandledThisLoop) {
mach_msg(reply, MACH_SEND_MSG, reply);
}
}

/// 执行加入到Loop的block
__CFRunLoopDoBlocks(runloop, currentMode);


if (sourceHandledThisLoop && stopAfterHandle) {
/// 进入loop时参数说处理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout) {
/// 超出传入参数标记的超时时间了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(runloop)) {
/// 被外部调用者强制停止了
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
/// source/timer/observer一个都没有了
retVal = kCFRunLoopRunFinished;
}

/// 如果没超时,mode里没空,loop也没被停止,那继续loop。
} while (retVal == 0);
}

/// 10. 通知 Observers: RunLoop 即将退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}

其内部是一个 do-while 循环。当调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。

1
2
3
4
5
6
7
8
9
/// 用DefaultMode启动
void CFRunLoopRun(void) {
CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}

/// 用指定的Mode启动,允许设置RunLoop超时时间
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

基于 Run Loop 的系统功能

如下所示为 App 启动后 Run Loop 的状态:

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
CFRunLoop {
current mode = kCFRunLoopDefaultMode
common modes = {
UITrackingRunLoopMode
kCFRunLoopDefaultMode
}

common mode items = {

// source0 (manual)
CFRunLoopSource {order =-1, {
callout = _UIApplicationHandleEventQueue}}
CFRunLoopSource {order =-1, {
callout = PurpleEventSignalCallback }}
CFRunLoopSource {order = 0, {
callout = FBSSerialQueueRunLoopSourceHandler}}

// source1 (mach port)
CFRunLoopSource {order = 0, {port = 17923}}
CFRunLoopSource {order = 0, {port = 12039}}
CFRunLoopSource {order = 0, {port = 16647}}
CFRunLoopSource {order =-1, {
callout = PurpleEventCallback}}
CFRunLoopSource {order = 0, {port = 2407,
callout = _ZL20notify_port_callbackP12__CFMachPortPvlS1_}}
CFRunLoopSource {order = 0, {port = 1c03,
callout = __IOHIDEventSystemClientAvailabilityCallback}}
CFRunLoopSource {order = 0, {port = 1b03,
callout = __IOHIDEventSystemClientQueueCallback}}
CFRunLoopSource {order = 1, {port = 1903,
callout = __IOMIGMachPortPortCallback}}

// Ovserver
CFRunLoopObserver {order = -2147483647, activities = 0x1, // Entry
callout = _wrapRunLoopWithAutoreleasePoolHandler}
CFRunLoopObserver {order = 0, activities = 0x20, // BeforeWaiting
callout = _UIGestureRecognizerUpdateObserver}
CFRunLoopObserver {order = 1999000, activities = 0xa0, // BeforeWaiting | Exit
callout = _afterCACommitHandler}
CFRunLoopObserver {order = 2000000, activities = 0xa0, // BeforeWaiting | Exit
callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv}
CFRunLoopObserver {order = 2147483647, activities = 0xa0, // BeforeWaiting | Exit
callout = _wrapRunLoopWithAutoreleasePoolHandler}

// Timer
CFRunLoopTimer {firing = No, interval = 3.1536e+09, tolerance = 0,
next fire date = 453098071 (-4421.76019 @ 96223387169499),
callout = _ZN2CAL14timer_callbackEP16__CFRunLoopTimerPv (QuartzCore.framework)}
},

modes = {
CFRunLoopMode {
sources0 = { /* same as 'common mode items' */ },
sources1 = { /* same as 'common mode items' */ },
observers = { /* same as 'common mode items' */ },
timers = { /* same as 'common mode items' */ },
},

CFRunLoopMode {
sources0 = { /* same as 'common mode items' */ },
sources1 = { /* same as 'common mode items' */ },
observers = { /* same as 'common mode items' */ },
timers = { /* same as 'common mode items' */ },
},

CFRunLoopMode {
sources0 = {
CFRunLoopSource {order = 0, {
callout = FBSSerialQueueRunLoopSourceHandler}}
},
sources1 = (null),
observers = {
CFRunLoopObserver >{activities = 0xa0, order = 2000000,
callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv}
)},
timers = (null),
},

CFRunLoopMode {
sources0 = {
CFRunLoopSource {order = -1, {
callout = PurpleEventSignalCallback}}
},
sources1 = {
CFRunLoopSource {order = -1, {
callout = PurpleEventCallback}}
},
observers = (null),
timers = (null),
},

CFRunLoopMode {
sources0 = (null),
sources1 = (null),
observers = (null),
timers = (null),
}
}
}

根据 modes 成员的状态可知,系统默认注册了 5 个 Mode:

  • kCFRunLoopDefaultMode:App 的默认 Mode,通常主线程是在这个 Mode 下运行的。
  • UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
  • UIInitializationRunLoopMode:在刚启动 App 时进入的第一个 Mode,启动完成后就不再使用。
  • GSEventReceiveRunLoopMode:接收系统事件的内部 Mode,通常用不到。
  • kCFRunLoopCommonModes:占位的 Mode,无实际作用。

AutoReleasePool

App 启动后,系统在主线程 Run Loop 中注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()

第一个 Observer 监听了一个事件:

  • kCFRunLoopEntry(即将进入 Loop):调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是 -2147483647,优先级最高,保证在其他所有回调之前创建。

第二个 Observer 监听了两个事件:

  • kCFRunLoopBeforeWaiting(即将进入休眠):调用 _objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 释放旧池并创建新池。
  • kCFRunLoopExit(即将退出 Loop):调用 _objc_autoreleasePoolPop() 来释放自动释放池。其 order 是 2147483647,优先级最低,保证在其他所有回调之后释放。

事件回调、Timer 回调一般在主线程执行。这些回调会被 Run Loop 创建的 AutoreleasePool 所环绕,所以不会出现内存泄漏,开发者也不必显式创建自动释放池。

事件响应

苹果注册了一个 Source1(Mach Port)用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()

当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。SpringBoard 只接收按键(锁屏/静音等)、触摸、加速、接近传感器等几种 Event,随后用 mach port 转发给需要的 App 进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。

_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括:识别 UIGesture、处理屏幕旋转、发送给 UIWindow 等。在此回调中完成的事件包括:UIButton 点击、touchesBegin/Move/End/Cancel 事件等。

手势识别

当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。

苹果注册了一个 Observer 监听 kCFRunLoopBeforeWaiting(即将进入休眠)事件,这个 Observer 的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行 GestureRecognizer 的回调。

当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

界面更新

当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay 方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。

苹果注册了一个 Observer 监听 kCFRunLoopBeforeWaiting(即将进入休眠) 和 kCFRunLoopExit(即将退出 Loop),回调执行的函数会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

定时器

NSTimer 其实就是 CFRunLoopTimerRef。一个 NSTimer 注册到 Run Loop 后,Run Loop 会为其重复的时间点注册事件。例如 10:00, 10:10, 10:20 这几个时间点。Run Loop为了节省资源,并不会在非常准确的时间点回调 Timer。Timer 有个属性叫做 Tolerance(宽容度),标示了当时间点到后,容许有多少最大误差。

如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。

CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。在快速滑动 TableView 时,即使一帧的卡顿也会让用户有所察觉。Facebook 开源的 AsyncDisplayLink 就是为了解决界面卡顿的问题,其内部也用到了 RunLoop。

PerformSelecter

当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。

当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

参考

  1. Run Loops
  2. 深入理解RunLoop
  3. RunLoop
  4. @autoreleasepool-内存的分配与释放
欣赏此文?求鼓励,求支持!