RunLoop知识点整理
概述
顾名思义,RunLoop翻译过来是跑圈的意思,其实是一个运行循环,也可以简单理解为是一个有退出条件的死循环。它有一下几个基本的作用:
- 保证程序的持续运行
- 处理App中的各种事件
- 节省CPU资源,提升程序性能
RunLoop在系统中应用十分广泛,以下模块都会用到RunLoop:
本质
Mach是XNU的内核,进程、线程和虚拟内存等对象通过端口port发消息进行通信,Runloop通过mach_msg()
函数发送消息.
如果没有port消息,内核会将线程置于等待状态mach_msg_trap()
。
如果有消息,判断消息类型处理事件,并通过modeItem
的callback回调
伪代码演示什么是RunLoop
- 如果没有RunLoop:
1
2
3
4
5
int main(int argc, char * argv[]) {
NSLog(@"execute main function");
return 0;
}
假如没有RunLoop,那么程序进入main函数后,就会打印“execute main function”后,立刻执行完并返回,而此时程序也就运行结束了,这显然是不合理的。
- 如果有了RunLoop:
1
2
3
4
5
6
7
8
9
int main(int argc, char * argv[]) {
static BOOL isRunning = YES;
do {
doSomeThing();// 处理各种任务和事件等
} while (isRunning);
return 0;
}
有RunLoop的情况下,相当于在程序结束前,里边执行了一个死循环,在这个循环中程序可以一直接受事件处理各种任务,程序不会立马退出,可以保持程序的执行状态。
以上伪代码演示了RunLoop的意义所在,但RunLoop的真面目并非像伪代码这么简单。
OC中的RunLoop
在main.m
文件的入口函数main()中是这样的:
1
2
3
4
5
6
7
8
9
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
这里UIApplicationMain
函数内部其实已经默认开启了一个RunLoop,因此UIApplicationMain函数其实一直没有返回,保持了程序的持续运行。而这个默认开启的RunLoop和主线程有关。
RunLoop对象
Cocoa中有2套RunLoop的API:
框架 | 相关类文件 | 语言 |
---|---|---|
Foundation | NSRunLoop | Objective-C |
CoreFoundation | CFRunLoop | C |
- CFRunLoop 是一套C语言的API
- NSRunLoop 是基于CFRunLoop 的OC封装
- CFRunLoop 开源,想研究其底层实现,需要研究CFRunLoop
RunLoop与线程
RunLoop与线程是息息相关的。RunLoop的存在,就是为了配合线程工作、保证线程的生命周期。
- 每条线程都可以有一个与之对应的唯一的RunLoop对象
- 主线程的RunLoop已经是自动创建并开启的;子线程的RunLoop需要手动创建并开启后,才会生效
- RunLoop在第一次获取时创建,在线程结束时销毁
RunLoop的获取
- Foundation
[NSRunLoop currentRunLoop];
// 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop];
// 获得主线程的RunLoop对象
- Core Foundation
CFRunLoopGetCurrent();
// 获得当前线程的RunLoop对象
CFRunLoopGetMain();
// 获得主线程的RunLoop对象
CFRunLoopGetMain 和 CFRunLoopGetCurrent 实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
CFRunLoopRef CFRunLoopGetMain(void) {
CHECK_FOR_FORK();
static CFRunLoopRef __main = NULL; // no retain needed
if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
return __main;
}
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
CAS:
CompareAndSwap 的缩写,翻译过来就是比较并替换。 CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。 更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
可以看出,runloop的获取其实是懒加载的。这里我们更加关注子线程runloop对象的获取。CFRunLoopGetCurrent
函数中根据子线程获取一个runloop的实现在_CFRunLoopGet0
函数里,下面是这个函数的实现源码。
_CFRunLoopGet0 实现
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
//获得runloop(创建runloop)
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
if (!__CFRunLoops) {
__CFUnlock(&loopsLock);
// 创建字典
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
// 创建主线程对应的runloop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
// 使用字典保存主线程-主线程对应的runloop
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
// 从字典中获取子线程的runloop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
if (!loop) {
// 如果子线程的runloop不存在,那么就为该线程创建一个对应的runloop
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
// 把当前子线程和对应的runloop保存到字典中
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
__CFUnlock(&loopsLock);
CFRelease(newLoop);
}
if (pthread_equal(t, pthread_self())) {
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
/// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}
从上面的代码可以看出,线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时(__CFFinalizeRunLoop
函数会在线程退出时执行,释放掉所有资源)。你只能在一个线程的内部获取其 RunLoop(主线程除外)。
RunLoop相关类
Core Foundation中关于RunLoop的5个类
1
2
3
4
5
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
CFRunLoopRef
代表RunLoop对象。
CFRunLoopModeRef
代表RunLoop的运行模式。
-
一个 RunLoop 包含若干个 Mode,每个Mode又包含若干个Source/Timer/Observer,而这些Source/Timer/Observer又称为modeItem
-
每次RunLoop启动时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。打个比方,空调有制冷、制热等模式,空调运行的时候必须指定其中的一个模式来运行。那么这当前运行的模式,就是空调的CurrentMode。
-
如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入
-
这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响
CFRunLoopModeRef数据结构(精简版)
1
2
3
4
5
6
7
8
9
struct __CFRunLoopMode {
CFStringRef _name; // mode的名称
CFMutableSetRef _sources0; // source0,集合
CFMutableSetRef _sources1; // source1,集合
CFMutableArrayRef _observers; // 观察者, 数组
CFMutableArrayRef _timers; // 定时器, 数组
__CFPortSet _portSet; // 端口,集合
};
这里有个概念叫 “CommonModes”:一个 Mode 可以将自己标记为”Common”属性(通过将其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 标记的所有Mode里。
系统默认注册了5个Mode:
mode | 作用 | 定义 | 使用频率 |
---|---|---|---|
kCFRunLoopDefaultMode | App的默认Mode,通常主线程是在这个Mode下运行 | 定义在CFRunLoop.h中 | 常用 |
UITrackingRunLoopMode | 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响 | 定义在UIApplication.h中 | 常用 |
UIInitializationRunLoopMode | 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用 | 不常用 | |
GSEventReceiveRunLoopMode | 接受系统事件的内部 Mode,通常用不到 | 不常用 | |
kCFRunLoopCommonModes | 这是一个占位用的Mode,不是一种真正的Mode | 定义在CFRunLoop.h中 | 常用 |
CFRunLoopSourceRef
代表事件源(输入源)
以前的分法:
- Port-Based Sources
- Custom Input Sources
- Cocoa Perform Selector Sources
现在的分法
- Source0:非基于Port的
- Source1:基于Port的
Source有两个版本:Source0 和 Source1。
-
Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
-
Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程,其原理在下面会讲到。
CFRunLoopTimerRef
代表基于时间的触发器。它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
CFRunLoopObserverRef
代表观察者,能够监听RunLoop的状态改变。每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个
1
2
3
4
5
6
7
8
9
10
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), //
kCFRunLoopBeforeTimers = (1UL << 1),
kCFRunLoopBeforeSources = (1UL << 2),
kCFRunLoopBeforeWaiting = (1UL << 5),
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopExit = (1UL << 7),
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
- kCFRunLoopEntry
即将进入Loop
进入事件处理循环之前,运行循环的入口。 每次调用CFRunLoopRun和CFRunLoopRunInMode都会发生一次此活动。
- kCFRunLoopBeforeTimers
即将处理timers
在事件处理循环内部,处理timer事件之前
- kCFRunLoopBeforeSources
即将处理sources
在事件处理循环内部,处理source事件之前
- kCFRunLoopBeforeWaiting
即将进入休眠
在事件处理循环内部,runloop休眠之前,等待source或者timer事件来触发。 如果以0秒的超时时间调用CFRunLoopRunInMode,则不会发生此活动。如果source0触发,则它也不会在事件处理循环的特定迭代中发生。
- kCFRunLoopAfterWaiting
刚从休眠中唤醒
在事件处理循环内部,在runloop唤醒之后但在处理将其唤醒的事件之前。 仅当runloop在当前循环期间进入睡眠状态时,才会发生此活动。
- kCFRunLoopExit
即将退出Loop
退出事件处理循环后,runloop的出口。 每次调用CFRunLoopRun和CFRunLoopRunInMode都会发生一次此活动。
- kCFRunLoopAllActivities
所有先前阶段的组合。
上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。runloop要运行,必须保证有事件可以处理,而事件就是Source或者Timer,对是否有Observer没要求。
RunLoop数据局结构(精简版)
1
2
3
4
5
6
7
struct __CFRunLoop {
pthread_t _pthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
};
这里有个概念叫 “CommonModes”:一个 Mode 可以将自己标记为”Common”属性(通过将其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 标记的所有Mode里。
你只能通过 mode name 来操作内部的 mode,当你传入一个新的 mode name 但 RunLoop 内部没有对应 mode 时,RunLoop会自动帮你创建对应的 CFRunLoopModeRef。对于一个 RunLoop 来说,其内部的 mode 只能增加不能删除。
用于mode管理的接口:
1
2
3
4
5
6
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 有两个:kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和 UITrackingRunLoopMode,你可以用这两个 Mode Name 来操作其对应的 Mode。
同时苹果还提供了一个操作 Common 标记的字符串:kCFRunLoopCommonModes (NSRunLoopCommonModes),你可以用这个字符串来操作 Common Items,或标记一个 Mode 为 “Common”。使用时注意区分这个字符串和其他 mode name。
线程、RunLoop、mode、modeItem的关系
RunLoop的处理逻辑
runloop启动的时候需要选择一个运行模式,检查运行模式是否为空(是否有”source | timer”),如果为空那么直接退出,不为空则开启 |
CFRunLoopRun 函数的实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 用DefaultMode启动
// RunLoop的主函数,是一个死循环
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
//CFRunLoopRunSpecific具体处理runloop的运行情况
//CFRunLoopGetCurrent() 当前runloop对象
//kCFRunLoopDefaultMode runloop的运行模式的名称
//1.0e10 runloop默认的运行时间,即超时为10的九次方
//returnAfterSourceHandled 回调处理
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
//如果runloop没有停止且没有结束则继续循环
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
在指定的mode下运行
1
2
3
4
5
6
7
// 用指定的Mode启动,允许设置RunLoop超时时间
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
CFRunLoopRunSpecific伪代码:
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
//RunLoop的实现
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
// 0.1 根据modeName找到对应mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
// 0.2 如果mode里没有source/timer 直接返回。
if (__CFRunLoopModeIsEmpty(currentMode)) return;
// 1.1 通知 Observers: RunLoop 即将进入 loop。---(OB会创建释放池)
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
// 1.2 内部函数,进入loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
Boolean sourceHandledThisLoop = NO;
int retVal = 0;
do {
// 2.1 通知 Observers: RunLoop 即将触发 Timer 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
// 2.2 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
// 2.3 RunLoop 触发 Source0 (非port) 回调。
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
// 2.4 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
if (hasMsg) goto handle_msg;
}
// 3.1 如果没有待处理消息,通知 Observers: RunLoop 的线程即将进入休眠(sleep)。--- (OB会销毁释放池并建立新释放池)
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}
// 3.2. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
/*
一个基于 port 的Source1 的事件。
一个 Timer 到时间了
RunLoop 启动时设置的最大超时时间到了
被手动唤醒
*/
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
}
// 3.3. 被唤醒,通知 Observers: RunLoop 的线程刚刚被唤醒了。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
// 4.0 处理消息。
handle_msg:
// 4.1 如果消息是Timer类型,触发这个Timer的回调。
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}
// 4.2 如果消息是dispatch到main_queue的block,执行block。
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}
// 4.3 如果消息是Source1类型,处理这个事件
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);
// 5.1 如果处理事件完毕,启动Runloop时设置参数为一次性执行,设置while参数退出Runloop
if (sourceHandledThisLoop && stopAfterHandle) {
retVal = kCFRunLoopRunHandledSource;
// 5.2 如果启动Runloop时设置的最大运转时间到期,设置while参数退出Runloop
} else if (timeout) {
retVal = kCFRunLoopRunTimedOut;
// 5.3 如果启动Runloop被外部调用强制停止,设置while参数退出Runloop
} else if (__CFRunLoopIsStopped(runloop)) {
retVal = kCFRunLoopRunStopped;
// 5.4 如果启动Runloop的modeItems为空,设置while参数退出Runloop
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
retVal = kCFRunLoopRunFinished;
}
// 5.5 如果没超时,mode里没空,loop也没被停止,那继续loop,回到第2步循环。
} while (retVal == 0);
}
// 6. 如果第6步判断后loop退出,通知 Observers: RunLoop 退出。--- (OB会销毁新释放池)
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}
RunLoop的应用
官方文档的场景:
Use ports or custom input sources to communicate with other threads.
Use timers on the thread.
Use any of the performSelector… methods in a Cocoa application.
Keep the thread around to perform periodic tasks.
翻译一下:
-
使用port或者自定义的数据源来与其他线程进行通信
-
在线程上使用timer
-
使用
performSelector...
方法簇 -
保持线程执行某些定期任务
此外还有以下场景,也能用到runloop:
- 开启常驻线程
- AutoreleasePool
- 监听runloop状态
RunLoop与AutoreleasePool
App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。
第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。
事件响应
苹果注册了一个 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 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。
当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。
界面更新
当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。
苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数: _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。
这个函数内部的调用栈大概是这样的:
1
2
3
4
5
6
7
8
9
10
11
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
QuartzCore:CA::Transaction::observer_callback:
CA::Transaction::commit();
CA::Context::commit_transaction();
CA::Layer::layout_and_display_if_needed();
CA::Layer::layout_if_needed();
[CALayer layoutSublayers];
[UIView layoutSubviews];
CA::Layer::display_if_needed();
[CALayer display];
[UIView drawRect];
定时器
NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个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 该方法也会失效。
GCD
当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。
RunLoop 的实际应用举例
AFNetworking
AFURLConnectionOperation 这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 RunLoop:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+ (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;
}
RunLoop 启动前内部必须要有至少一个 Timer/Observer/Source,所以 AFNetworking 在 [runLoop run] 之前先创建了一个新的 NSMachPort 添加进去了。通常情况下,调用者需要持有这个 NSMachPort (mach_port) 并在外部线程通过这个 port 发送消息到 loop 内;但此处添加 port 只是为了让 RunLoop 不至于退出,并没有用于实际的发送消息。
1
2
3
4
5
6
7
8
9
10
- (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];
}
当需要这个后台线程执行任务时,AFNetworking 通过调用 [NSObject performSelector:onThread:..] 将这个任务扔到了后台线程的 RunLoop 中。
AsyncDisplayKit
AsyncDisplayKit 是 Facebook 推出的用于保持界面流畅性的框架,其原理大致如下:
UI 线程中一旦出现繁重的任务就会导致界面卡顿,这类任务通常分为3类:排版,绘制,UI对象操作。
排版通常包括计算视图大小、计算文本高度、重新计算子式图的排版等操作。 绘制一般有文本绘制 (例如 CoreText)、图片绘制 (例如预先解压)、元素绘制 (Quartz)等操作。 UI对象操作通常包括 UIView/CALayer 等 UI 对象的创建、设置属性和销毁。
其中前两类操作可以通过各种方法扔到后台线程执行,而最后一类操作只能在主线程完成,并且有时后面的操作需要依赖前面操作的结果 (例如TextView创建时可能需要提前计算出文本的大小)。ASDK 所做的,就是尽量将能放入后台的任务放入后台,不能的则尽量推迟 (例如视图的创建、属性的调整)。
为此,ASDK 创建了一个名为 ASDisplayNode 的对象,并在内部封装了 UIView/CALayer,它具有和 UIView/CALayer 相似的属性,例如 frame、backgroundColor等。所有这些属性都可以在后台线程更改,开发者可以只通过 Node 来操作其内部的 UIView/CALayer,这样就可以将排版和绘制放入了后台线程。但是无论怎么操作,这些属性总需要在某个时刻同步到主线程的 UIView/CALayer 去。
ASDK 仿照 QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制:即在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。 具体的代码可以看这里:_ASAsyncTransactionGroup。