四、事件处理机制(RunLoop)

Reference:
深入理解RunLoop
RunLoop 详解

程序运行时代码都是顺序执行的,执行完毕程序就结束退出了,了解APP工作机制的一个基础,是弄清楚APP如何实现在内存中常驻的。

概述

代码在程序的线程中顺序执行,当程序所有的线程中代码都运行完毕,程序就结束并退出了,iOS与MacOS中通过RunLoop机制来让线程可以一直保持运行,并循环的处理各类事件,从而让APP常驻在内存中持续工作 。系统的某些任务也依托RunLoop来完成,了解RunLoop的运行机制将有助于了解系统任务的机制,同时理解了RunLoop能清楚一个线程中(特别是主线程中)各类任务的代码执行顺序,还将有助于多线程并发编程时的程序设计。

概览

RunLoop

要点

  • RunLoop就是让线程保持一直运行,并可以循环处理事件的机制;
  • RunLoop线程一一对应,代码中不能直接创建,而是提供获取主线程和当前线程RunLoop的API,在第一次调取API时,API内部负责创建RunLoop,子线程的RunLoop需要手动启动;
  • 一个RunLoop包含若干个Mode,每个Mode又包含若干个Source/Timer/Observer;
  • RunLoop必须指定一个Mode进行运行,运行后如果Mode中一个items都没有,将会退出运行;
  • kCFRunLoopCommonModes是一个伪Mode,用它添加的items,在其他“CommonMode”运行时,也能得到触发,NSTimer通常需要通过此Mode添加到RunLoop中;
  • Source分为Source0和Source1,Source0需要手动显示唤醒RunLoop来处理事件,可用于线程间发送消息,Source1是基于端口的,端口有消息能自动唤醒RunLoop来处理事件,用于监听内核端口的消息;
  • Timer是在计算好的预设时间点由内核发送时间通知,唤醒RunLoop来处理Timer事件;
  • Observer是RunLoop的观察者,RunLoop在某些活动节点会触发其回调,可以据此在对应的时机安排自己APP需要执行的任务;
  • RunLoop的休眠/唤醒是通过Mach陷阱切换程序的应用态/内核态来实现的;
  • Perform Selector系列方法中延迟派发、派发给其他线程、以及当前线程异步派发时,是通过创建源并加入到对应RunLoop中来实现的,必须要目标线程的RunLoop运行,派发的Selector才会被执行,Perform Selector添加的源会在执行后从RunLoop中移除;
  • 用GCD为主线程派发任务,也是包装成待处理的Source1,再添加到主线程RunLoop中来实现的,但GCD为子线程(并发队列)派发任务,不是通过RunLoop实行的;
  • UIEvent是UIKit在主线程中注册一个Source1,通过端口接收到Spring Board转发的IOHIDEvent,再在回调中处理,最后包装成UIEvent的;

详解

1.什么是RunLoop

RunLoop是一种iOS与MacOS中让线程保持一直运行,并可以循环处理事件的机制。RunLoop在CoreFoundation中通过CFRunLoopRef对象来实现,其提供纯C函数的、线程安全的API;同时还有基于CFRunLoopRef更上层的封装:NSRunLoop,其提供面向对象的API,但不是线程安全的。

2.RunLoop与线程的关系

RunLoop与线程是一一对应的,CoreFoundation与NSFoundation中都不提供直接创建RunLoop的API,而是提供了获取主线程RunLoop和当前线程RunLoop的API,在第一次调取线程的RunLoop获取API时,函数内部才为线程创建其RunLoop,并以线程为key,RunLoop为value,保存在一个全局字典中。

3.RunLoop的相关概念

RunLoop中的结构如图:

RunLoop_Modes_Items

一个RunLoop中包含若干个Mode,每个Mode中又包含若干个Source、Timer和Observer,Source、Timer和Observer统称为items,一个item可以添加到不同的Mode,重复添加到同一个Mode没有效果;启动RunLoop时需要指定一个Mode,Mode中至少要有一个item,否则RunLoop会立即退出;RunLoop运行期间指定Mode下的items的回调会触发,如果指定运行的Mode属于”CommondMode”,则标记为 kCFRunLoopCommonModes的items的回调也会触发要切换Mode,必需停止RunLoop,再指定新的Mode重新启动RunLoop

在CoreFoundation中Mode和items对应的类为:CFRunLoopModeRef、CFRunLoopSourceRef、CFRunLoopTimerRef和CFRunLoopObserverRef,下面将分别介绍它们的特性。

CFRunLoopModeRef

CFRunLoopModeRef类没有对外暴露,而是提供了一系列根据mode name来管理Mode和Mode下的items(Source、Timer和Observer)的API:

1
2
3
4
5
6
7
8
9
10
// 管理Mode的接口
CFRunLoopRunInMode(CFStringRef modeName, ...);
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
// 管理Mode下items的接口
CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);

提供的接口只能通过mode name来管理mode,当传入一个新的mode name,而RunLoop内又没有对应的Mode时,RunLoop会自动创建对应的CFRunLoopModeRef;并没有提供删除Mode的API,所以RunLoop的Mode只能增加不能删除。

系统为主线程的RunLoop默认注册了五个Mode:

  1. kCFRunLoopDefaultMode:默认Mode,主线程通常也是在此Mode下运行;
  2. UITrackingRunLoopMode:界面追踪Mode,当需要追踪触摸滑动时,主线程会被切换到此Mode,避免其他items对界面滑动的影响;
  3. UIInitializationRunLoopMode:APP刚启动时使用的Mode,启动完成后就不再使用;
  4. GSEventReceiveRunLoopMode:接收系统事件的内部Mode,开发中通常用不到;
  5. kCFRunLoopCommonModes:占位Mode,不是一个真正的Mode,可以用来添加items,不能用来启动RunLoop;

上述Mode中开发相关的有:kCFRunLoopDefaultMode、UITrackingRunLoopMode和kCFRunLoopCommonModes,其中kCFRunLoopDefaultMode和UITrackingRunLoopMode是真正的Mode,而kCFRunLoopCommonModes是一个占位Mode。用kCFRunLoopCommonModes添加的items,在RunLoop以一个“CommondMode”运行时,也能得到触发。可以用CFRunLoopAddCommonMode将一个Mode标记为”CommondMode“(既将Mode添加到CFRunLoopRef的_commonModes集合中),kCFRunLoopDefaultMode和UITrackingRunLoopMode都是”CommondMode“。

事件队列中判断Source和Timer的Block是否触发代码逻辑大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static Boolean __CFRunLoopDoBlocks(CFRunLoopRef rl, CFRunLoopModeRef rlm) { 
... ...
while (item) {
struct _block_item *curr = item;
item = item->_next;
Boolean doit = false;
if (CFStringGetTypeID() == CFGetTypeID(curr->_mode)) {
doit = CFEqual(curr->_mode, curMode) || (CFEqual(curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(commonModes, curMode));
}
else {
doit = CFSetContainsValue((CFSetRef)curr->_mode, curMode) || (CFSetContainsValue((CFSetRef)curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(commonModes, curMode));
}
if (!doit) prev = curr;
if (doit) {
... ...
}
}
... ...
return did;
}

CFRunLoopSourceRef

RunLoop的事件源有两个:输入源和定时源,输入源即Source,可以通过为Mode添加Source,来为RunLoop添加需要处理的事件,在Cocoa层官方将Source分为三类:

  • Port-Based Sources
  • Custom Input Sources
  • Cocoa Perform Selector Sources

CoreFoundation中输入源则分为非基于端口的Source0和基于端口的Source1,分别对应Custom Input Sources和Port-Based Sources,Cocoa Perform Selector Sources是Perform Selector系列方法创建的源,Perform Selector延迟派发时创建的是定时源,派发给其他线程或当前线程异步派发时创建的Source0,Perform Selector创建的源,在执行完后会自动从RunLoop中移除

  • Source0:只包含一个回调(函数指针),需要应用手动触发,不能主动唤醒RunLoop,使用时需要先调用CFRunLoopSourceSignal(source),将Source标记为待处理,再手动调用CFRunLoopWakeUp(runloop),唤醒RunLoop处理Source的事件。
  • Source1:包含一个mach_port和一个回调(函数指针),由mach_port驱动,可以主动唤醒RunLoop,通常用于通过内核和其他线程互相发送消息。

CFRunLoopTimerRef

Timer就是定时源,是RunLoop的另一个事件源,也可以通过为Mode添加Timer,来为RunLoop添加需要处理的事件。CFRunLoopTimerRef包含一个时间长度和一个回调(函数指针),它与NSTimer是toll-free bridged的,可以混用。当Timer加入到RunLoop之后,会基于XNU内核的mk_timer或GCD计时器(根据CoreFoundation的版本不同而不同),在计算好的预设时间点由内核发送通知,唤醒RunLoop处理Timer的事件。

CFRunLoopObserverRef

Observer是RunLoop的观察者,包含一个回调(函数指针),当RunLoop运行到特定状态时会触发回调,并将状态传递给回调函数,会触Observer回调的状态包括:

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

4.RunLoop的核心逻辑

CoreFoundation的版本不同,RunLoop的核心源码可能不一致,但逻辑不会有太大的出入,整体可以参考以下对CoreFoundation CF-855.17版本源码删减后的逻辑整理:

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
/// 公开接口一:用DefaultMode启动
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

/// 公开接口二:用指定的Mode启动(允许设置RunLoop超时时间)
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

/// RunLoop的实现
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;

/// 首先根据modeName找到对应mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);

/// 如果mode里没有source/timer/observer, 直接返回。
if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
Boolean did = false;
return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
}

CFRunLoopModeRef previousMode = rl->_currentMode;
rl->_currentMode = currentMode;
int32_t result = kCFRunLoopRunFinished;

/// 1. 通知 Observers: RunLoop 即将进入 loop。
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);

result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);

/// 10. 通知 Observers: RunLoop 即将退出。
if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

rl->_currentMode = previousMode;

return result;
}

/// 内部核心函数,进入loop
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {

/// 如果当前是主线程RunLoop,且当前mode的第一次运行,会分发一个GCD端口(用于第5步)
mach_port_name_t dispatchPort = MACH_PORT_NULL;
Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));
if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) dispatchPort = _dispatch_get_main_queue_port_4CF();

/// 给当前模式分发GCD定时端口
mach_port_name_t modeQueuePort = MACH_PORT_NULL;
if (rlm->_queue) {
modeQueuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue);
if (!modeQueuePort) {
CRASH("Unable to get port for run loop mode queue (%d)", -1);
}
}

/// 初始化一个GCD计时器,用于第7步对RunLoop进行超时唤醒
dispatch_source_t timeout_timer = NULL;
struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context));
if (seconds <= 0.0) { // instant timeout
seconds = 0.0;
timeout_context->termTSR = 0ULL;
} else if (seconds <= TIMER_INTERVAL_LIMIT) {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, DISPATCH_QUEUE_OVERCOMMIT);
timeout_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_retain(timeout_timer);
timeout_context->ds = timeout_timer;
timeout_context->rl = (CFRunLoopRef)CFRetain(rl);
timeout_context->termTSR = startTSR + __CFTimeIntervalToTSR(seconds);
dispatch_set_context(timeout_timer, timeout_context); // source gets ownership of context
dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout);
dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel);
uint64_t ns_at = (uint64_t)((__CFTSRToTimeInterval(startTSR) + seconds) * 1000000000ULL);
dispatch_source_set_timer(timeout_timer, dispatch_time(1, ns_at), DISPATCH_TIME_FOREVER, 1000ULL);
dispatch_resume(timeout_timer);
} else { // infinite timeout
seconds = 9999999999.0;
timeout_context->termTSR = UINT64_MAX;
}

Boolean didDispatchPortLastTime = true;
int32_t retVal = 0;
do {
uint8_t msg_buffer[3 * 1024];
mach_msg_header_t *msg = NULL;
mach_port_t livePort = MACH_PORT_NULL;
__CFPortSet waitSet = rlm->_portSet;

/// 2. 通知 Observers: RunLoop开始处理Timer
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: RunLoop即将处理Source0(非port)回调
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);

/// 执行可能即时提交到队列中的block
__CFRunLoopDoBlocks(rl, rlm);

/// 4. 处理Source0
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);

/// 如果Source0中有待处理的事件,执行Source0提交到队列中的block
if (sourceHandledThisLoop) {
__CFRunLoopDoBlocks(rl, rlm);
}

/// 标志是否等待端口唤醒
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);

/// 5. 如果是主线程(dispatchPort是主线程才会被赋值)在进入休眠前会检查是否有通过GCD派发过来的Source1,如果有,跳过休眠到第9步进行处理
if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
/// 首次循环是不会进入检查(didDispatchPortLastTime为true)
msg = (mach_msg_header_t *)msg_buffer;
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0)) {
goto handle_msg;
}
}
didDispatchPortLastTime = false;

/// 6.通知 Observers: RunLoop即将进入休眠(sleep)
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);

__CFRunLoopSetSleeping(rl);

/// 7. 调用mach_msg等待接受mach_port的消息,线程将进入休眠, 直到被下面某一个事件唤醒:
/// • 某个基于port的Source1有消息到达
/// • 某个Timer的时间到了
/// • RunLoop为自身设定的超时时间到了
/// • 被其他调用者手动显式唤醒

msg = (mach_msg_header_t *)msg_buffer;
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);

__CFRunLoopUnsetSleeping(rl);

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

/// 事件处理的跳转标准
handle_msg:

/// 9. 处理未处理的事件
if (MACH_PORT_NULL == livePort) {
/// 无端口唤醒,RunLoop正常运行不会有此情况
CFRUNLOOP_WAKEUP_FOR_NOTHING();
} else if (livePort == rl->_wakeUpPort) {
/// 被自身的CGD超时计时器唤醒,无需做任何处理
CFRUNLOOP_WAKEUP_FOR_WAKEUP();
}
else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
/// 被定时器通知唤醒,处理定时器事件
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
__CFArmNextTimerInMode(rlm, rl);
}
}
else if (livePort == dispatchPort) {
/// 主线程被GCD派发的事件唤醒,或由第5步直接跳转过来,处理派发的事件
CFRUNLOOP_WAKEUP_FOR_DISPATCH();
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else {
/// 被source1唤醒,处理Source1
CFRUNLOOP_WAKEUP_FOR_SOURCE();
mach_msg_header_t *reply = NULL;
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
}

/// 执行以上各类事件时加入到队列中的block
__CFRunLoopDoBlocks(rl, rlm);

/// 跳出循环,停止RunLoop的各类情况
if (sourceHandledThisLoop && stopAfterHandle) {
/// 启动RunLoop时即指明处理完事件就返回
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
/// RunLoop运行超过启动时指明的超时时间
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
/// 被外部调用者手动终止了RunLoop
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
/// RunLoop被标记为已停止了
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
/// 当前Mode中Source/Timer/Observer都被移除了
retVal = kCFRunLoopRunFinished;
}
} while (0 == retVal);

/// 释放RunLoop自身的超时GCD计时器
if (timeout_timer) {
dispatch_source_cancel(timeout_timer);
dispatch_release(timeout_timer);
} else {
free(timeout_context);
}
return retVal;
}

值得一提的是CF-855.17版本是CoreFoundation使用mk_timer和GCD计时器的一个过渡版本,源码中有分使用mk_timer和GCD计时器的条件编译,以上代码逻辑的整理中,只参照了使用GCD计时器的源码。

RunLoop核心代码逻辑的关键流程概述如下:

  1. 通知Observer:RunLoop进入Loop;
  2. 通知Observer:RunLoop开始处理Timer;
  3. 通知Observer:RunLoop即将处理Source0;
  4. 执行Source0加入到事件队列中的Block;
  5. 如果是主线程RunLoop,检查是否有通过GCD派发给主线程的待处理的Source1:
    • 没有:继续下一步;
    • 有:跳到第9步去处理Source1;
  6. 通知Observer:RunLoop即将进入休眠;
  7. 休眠,等待被唤醒:
    • 某个基于port的Source1有消息到达;
    • 某个Timer的时间到了;
    • RunLoop为自身设定的超时时间到了;
    • 被其他调用者手动显式唤醒;
  8. 通知Observer:RunLoop刚被唤醒;
  9. 开始处理未处理的事件:
    • 超时唤醒,不用做任何处理;
    • Timer到时了,处理Timer的事件;
    • 如果是主线程,处理GCD派发过来的事件;
    • 处理基于port的Source1;
  10. 通知Observer:已经退出Loop;

RunLoop的代码内部是一个do-while循环,循环的执行上述流程的第2-9步,以下情况会导致此次流程后跳出循环,退出RunLoop

  1. 启动RunLoop时的参数指明:执行一遍事件处理流程就退出循环;
  2. 超过了启动RunLoop时指定的超时时间;
  3. 被外部调用者手动显式的终止了RunLoop;
  4. RunLoop被标记为已停止(_stopped)了;
  5. 当前Mode中Source/Timer/Observer都被移除了;

RunLoop在需要时被唤醒,在不需要时进行休眠,其原理是通过mach_msg( )调用了一个Mach陷阱:mach_msg_trap( ),将线程由用户态切换为内核态,在内核态下等待消息,当有消息到达时又返回用户态进行处理,其逻辑示意如图:

RunLoop_Trap

5.系统中对RunLoop的应用

Perform Selector

Perform Selector系列方法延迟派发创建的是定时源,派发给其他线程或当前线程异步派发时创建的Source0,再添加到指定线程的RunLoop中。所以如果使用Perform Selector延迟派发、派发给其他线程、以及当前线程异步派发时,要注意判断目标线程的RunLoop是否在运行,如果未运行则派发的任务就不会被执行。

GCD

使用GCD为主线程派发任务,会唤醒主线程RunLoop,并对派发的事件进行处理,在每次主线程RunLoop休眠前,还会检查是否有GCD派发过来的事件,如果有,则会跳过休眠,开始对事件进行处理。但以上RunLoop中的逻辑,仅限于主线程中的RunLoop,通过GCD为其他线程派发的任务,则是由libDispatch处理,所以GCD将任务派发给子线程时,也不需要子线程的RunLoop处于运行状态。

UIEvent

系统为APP注册了一个Source1用来接收操作系统的事件。当一个硬件事件(触摸/锁屏/摇晃等)发生后,先由IOKit.framework生成一个IOHIDEvent,并由SpringBoard接收,SpringBoard再通过mach_port转发给需要的APP;APP中系统注册的那个Source1接收到mach_port中传递过来的消息后,唤醒主线程RunLoop,在其回调中调用_UIApplicationHandleEventQueue( )进行APP内部的分发;_UIApplicationHandleEventQueue( )中会把IOHIDEvent包装成UIEvent,再通过APP的响应链传递给对应的响应者。

NSTimer

NSTimer其实就是CFRunLoopTimerRef,他们之间是toll-free bridged的(Toll-Free Bridged Types)。根据版本不同RunLoop的Timer可能基于内核的mk_timer或者GCD的timer来实现的,Timer并不是绝对准时的,而是有一个宽容度(Tolerance),实际执行的时间可能在这个宽容度允许的误差之内,如果因为执行队列中其他事件造成时间超过了宽容度所允许的误差,那么这次Timer的block不会延后执行,而是会被跳过。

NSTimer默认是添加到RunLoop的kCFRunLoopDefaultMode中的,如果需要NSTimer在RunLoop切换为其他Mode运行时也会正确触发,需要通过kCFRunLoopCommonModes来添加NSTimer。

AutoreleasePool

APP启动后,系统为主线程RunLoop注册了两个Observer来管理主线程的自动释放池:

  • 第一个Observer只观察kCFRunLoopEntry:在其回调中调用_objc_autoreleasePoolPush( )创建自动释放池;
  • 第二个Observer观察两个事件:在其回调中观察到kCFRunLoopBeforWaiting,会调用_objc_autoreleasePoolPop( ),再调用_objc_autoreleasePoolPush( ),以释放旧池并创建新池;回调中观察到kCFRunLoopExit,会调用_objc_autoreleasePoolPop( )来释放自动释放池;

子线程中的自动释放池会在使用时懒加载,在线程退出时释放,所以一般子线程中也无需考虑内存自动释放的问题,但是如果通过运行子线程的RunLoop来让子线程常驻,可能需要考虑为某些自动释放对象添加自动释放池来避免内存泄漏。

UIKit

当更改了UI之后,比如改变了Frame、更新了UIView/CALayer的层次、或手动调用了UIView/CALayer的setNeedsLayout/setNeedsDisplay等,这个UIView/CALayer会被标记为待处理,然后提交到一个全局容器中去。系统为主线程注册了一个Observer来刷新UI,在其回调中,当观察到kCFRunLoopBeforWaiting和kCFRunLoopExit时,会调用一个函数去遍历所有待处理的UIView/CALayer,为它们执行实际的绘制和调整,来完成界面的刷新。

NSURLConnection与NSURLSession

NSURLConnection已经被NSURLSession取代,但底层的部分逻辑却在沿用,同时NSURLConnection对RunLoop的运用也可作为多线程编程设计的参考:开始网络任务时,会为NSURLConnection设置一个delegate,除了这个开始任务时的delegate线程,NSURLConnection又注册了两个新线程com.apple.NSURLConnectionLoader和com.apple.CFSocket.private,三个线程间的消息交流如图所示:

RunLoop_network

CFSocket线程负责处理底层socket连接,NSURLConnectionLoader则负责居中调度。开始一个网络任务时,NSURLConnection创建了4个Source0,并添加到了delegate线程RunLoop的DefaultMode中,NSURLConnectionLoader线程本身通过一个Source1来接收底层CFSocket线程根据网络连接产生的通知,在NSURLConnectionLoader线程的Source1回调中,又通过delegate线程的4个Source0把相关的通知传递给delegate线程,在这些Source0的回调中再执行真正的delegate回调方法。

6.三方库对RunLoop的应用

AFNetworking

在AFNetworking2.x中通过开启子线程的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
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}

+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}

- (void)start {
[self.lock lock];
if ([self isCancelled]) {
[self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
} else if ([self isReady]) {
self.state = AFOperationExecutingState;
[self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
}
[self.lock unlock];
}

AFNetworking2.x是基于NSURLConnection的,AFNetworking在常驻子线程中异步的start所有NSURLConnection,也在此线程中接收所有NSURLConnection的delegate回调,所以需要子线程常驻。

由于NSURLConnection需要delegate线程保持活跃来接收回调,如果维持一个常驻子线程来管理NSURLConnection的网络请求则开销太大了,在NSURLSession中已经改为指明delegateQueue(NSOperationQueue)来处理网络回调,AFNetworking3.x之后网络请求改为使用NSURLSession,所以也不再需要常驻子线程来处理网络回调了。

值得一提的是NSURLSession的delegateQueue的maxConcurrentOperationCount通常需要设为1,来将代理队列设置为一个串行队列,这是因为即使设置为并发队列,在回调中通常也需要加锁来处理,实际也是串行执行,直接设置为串行队列,则无需再添加加锁等操作。

ReactiveCocoa

在RAC的一个Testing方法中,通过如下方法来使用RunLoop:

1
2
3
do {
[NSRunLoop.mainRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
} while (!done);

上面代码是用来满足RAC单元测试时的需求:短暂的运行起主线程,以接收通过主线程派发的RACSignal。这种在do-while循环中设置一个超时时间来运行RunLoop的方式值得参考,在现在的ARC下,它其实起到了定时释放旧池(自动释放池)并创建新池的作用(每一次循环,ARC会自动为其添加自动释放池的创建与释放)。

Texture

Texture原名AsyncDisplayKit,是一个界面性能优化框架,相较于UIKit把界面元素的创建、绘制、渲染、销毁等任务都在主线程中顺序完成,它把界面显示的相关任务进行了拆分,再把可以放到子线程的任务并发的执行,把可以推迟的任务延后执行,把可以合并的任务合并显示,以此来保证界面的流畅性。

Texture中也有对RunLoop的相关应用,相关逻辑如图所示:
ios_vsync_runloop

Texture把界面元素中必须在主线程完成的部分任务,先封装到一个全局容器中,然后在主线程RunLoop中注册了一个Observer,观察的事件和CoreAnimation的Observer观察的事件相同:kCFRunLoopBeforeWaiting和kCFRunLoopBeforeExit,但回调优先级低于CoreAnimation的回调,这样在RunLoop每次休眠前或退出时,在CoreAnimation的回调处理完成后,Texture再执行自己全局容器中提交的任务,此时这些必须在主线程完成的任务需要依赖的任务,便可能已经异步、并发的完成,此处Texture也完成了将这些异步、并发的操作同步到主线程中去。

-------------This article is over, thank you for reading -------------