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 的线程。如:UIEventCFSocket、普通函数调用、系统调用等。
  • Port-Based SourcesSource1):系统底层的 Port 事件(Mac Port),如 CFMachPortCFMessagePort。一般用于通过内核和其他线程相互发送消息,应用层很少使用。这种 Source 可以主动唤醒 Run Loop 的线程。

Timer Source

Timer Source 即定时器事件。本质上仍然属于 Port-Based Source,所有的 Timer 都共用一个端口“Mode Timer Port”。

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-内存的分配与释放