带着问题思考 Questions
- RunLoop 他是什么?
- RunLoop 结构是什么样的?
- 为什么要是这样的结构,其他的样子不可以吗?
- RunLoop 内部分析?
- 系统中有哪些功能使用了 runloop?
- 开发中如何使用 RunLoop
引用 CFRunLoopRef 源码:CF-1153.18.tar.gz
RunLoop 来源
- 一般来说,thread 只运行一次,然后终止. 像命令行驱动程序。但是这样的程序没有用户交互性
- 为了使程序增加用户交互性,就需要一个机制让程序不退出等待用户操作。使用Event Loop设计模式,让 thread 关联一个 event loop,监听 event,然后调用事件的 reactor 处理事件通常是交由 event loop 关联的 thread 处理。这样使 thread 保活
1 | function main |
而对于没有 runloop 的thread
1 |
|
结果只会打印 task1,因为 thread 没有 runloop,所以执行完当前任务 task1 以后会自动退出
1 | func withoutRunloop() { |
打印 task1,task2(),只有当 Port 从 runloop 中移除这个线程就会退出,否则他的生命周期由 runloop 维护着,具体看下文
Event loop 机制
Event Loop 在很多平台上都有,无论是 GUI,开始系统事件都需要 Event Loop 模式来处理交互程序
环境 | Event_Loop对应名称 |
---|---|
OSX/iOS | RunLoop |
Windows | 消息队列 |
Linux | epoll,select |
消息队列使得调用方跟被调用方之间解耦
OSX/iOS 中的 Event Loop
RunLoop 特点:
- 他是与线程相关的基础架构的一部分,充当着循环处理、调度事件/转发消息的角色,管理thread运行状态。
- 它使得线程不会执行完单个任务后就立刻结束
- 让线程在没有任务时保持休眠状态
- 在需要处理消息时被立刻唤醒
- RunLoop 是个对象,每个线程都可以有对应的 runloop。runloop的管理机制并不完全是自动的,有时需要设计好 runloop 的运行时间和事件处理回调。
- 除了主线程外,子线程需要开发者手动去配置并运行它的 runloop
- 主线程的 runloop 已经由系统自动配置并运行了。
- runloop 在 Cocoa 和 Core Foundation 有两个对应的类:NSRunLoop 和 CFRunLoop
Run Loop 内部执行流程
线程的 run loop 每次运行都会处理待决的事件,并为绑定的所有 observer 生成通知。次序如下:
如果mode里没有source/timer/observer, 直接返回。
- 通知
observer
已经进入 run loop (kCFRunLoopEntry) - 通知
observer
有 timer 将要触发 (kCFRunLoopBeforeTimers) - 通知
observer
有非基于端口的 input source(source0) 将要触发 (kCFRunLoopBeforeSources) - 触发所有已就绪的非基于端口的 input source(source0)
- 如果一个基于端口的 input source(source1, 系统触发) 已就绪并等待触发,立即处理事件,并转至第 9 步
- 通知
observer
线程即将休眠 (kCFRunLoopBeforeWaiting) - 让线程休眠,直到被以下条件唤醒:
- 有基于端口的 input source 事件到达
- timer 触发
- run loop 设定的超时时间到了
- run loop 被手动唤醒
- 通知
observer
线程刚刚被唤醒 (kCFRunLoopAfterWaiting) - 处理待决事件
- 如果用户定义的 timer 触发了,处理 timer 事件并重启 run loop,跳回到第 2 步
- 如果 input source 触发了,分发事件
- 如果 run loop 被唤醒且没有超时,重启 run loop,跳回到第 2 步
- 通知
observer
已经退出 run loop (kCFRunLoopExit)
对应简化源码
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 int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
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);
}实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。

RunLoop底层实现
核心是源码中的第 7 步:
7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒
- 一个基于 port 的Source 的事件。
- 一个 Timer 到时间了
- RunLoop 自身的超时时间到了
- 被其他什么调用者手动唤醒
mach_msg() 函数实际上是调用了一个 Mach 陷阱 (trap),即函数mach_msg_trap(),陷阱这个概念在 Mach 中等同于系统调用。当你在用户态调用 mach_msg_trap() 时会触发陷阱机制,切换到内核态;内核态中内核实现的 mach_msg() 函数会完成实际的工作,如下图:
使用 xcode,当程序不执行操作的时候,click debug 的暂停main_thread_mach_msg
在没有事件处理时:
Thread 1 Queue : com.apple.main-thread (serial)
com.apple.uikit.eventfetch-thread (6)
这两个线程的runloop都停留在mach_msg_trap状态,等待事件发生。
其他线程都是 _workq_kernreturn:
等待 spinlock
技术,从而执行队列中的下一个 task(或者退出)
RunLoop结构
Mode
首先要知道 Mode 的作用:为了减少runloop监听的source/timer/observer 的个数
RunLoop 和 Mode 简化源码
1 | typedef struct __CFRunLoopMode *CFRunLoopModeRef; |
Input Sources:
- Port-Based Sources:监听App的 Mach Port,由内核发出信号,输入源收到信号后,执行相关的例程。(sources1)
- Custom Input Sources:监听自定义的输入源,需要在其它线程手动发送信号,输入源收到信号后,执行相关的例程。(sources0)
- Cocoa Perform Selector Sources:Cocoa中自定义的输入源,目的是在不同线程中执行任务,同一线程中的任务是顺序执行的,当任务执行完成后系统会自动移除这个源。(注意:在目标线程中执行任务时,这个目标线程必须有活跃的RunLoop)(sources0)
时间源 time sources
需要监听runloop的当前状态的:监听者 observers
RunLoop 的每次运行都会在某个特定模式下,而且只有这个模式所包含的 item 集合才会参与发送事件(被监听)和接收通知。如果改 item 没有加入到指定模式下的runloop,那么该模式下就不会执通知or监听该 item,如果没有 item 那么会进入休眠状态。
- mode 中需要注意的 default 和 tracing 这两种,在实际开发中会经常使用
Mode | 名称 | 描述 |
---|---|---|
Default | NSDefaultRunLoopMode (Cocoa), kCFRunLoopDefaultMode (Core Foundation) | App的默认运行模式,通常主线程是在这个运行模式下运行 |
Connection | NSConnectionReplyMode (Cocoa) | Cocoa 中结合 NSConnection 使用,用于监听回复(Reply),极少用到。(已弃用) |
Modal | NSModalPanelRunLoopMode (Cocoa) | Cocoa 中 modal panel 使用它接收与之相关 Source 的事件 |
Event tracking | NSEventTrackingRunLoopMode (Cocoa), UITrackingRunLoopMode (Cocoa Touch) | 跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响) |
Common modes | NSRunLoopCommonModes (Cocoa), kCFRunLoopCommonModes (Core Foundation) | 这是一组可配置的通用集合,如果将某个 input source 注册到该模式下,那么 input source 在通用模式集合中的每个 mode 中都会注册。Cocoa 框架中的 Common modes 默认包含 Default, Modal, Event tracking 三种 Mode;Core Foundation 只包含 Default,可以使用 CFRunLoopAddCommonMode 函数向集合中添加自定义 Mode。 |
Common modes 不是一个mode,而是一个 modes 集合!
eg: 在 Cocoa 框架中,如果 Sources, timers, observers 添加到 CommonModes中,那么 Sources, timers, observers 会被 CommonModes 结合中(Default, Modal, Event tracking)所有模式共享!
1 | void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFRunLoopMode mode); |
一旦 mode 添加到 runloop 的_commonModes以后就不可以被删除了!此时需要创建新的 runloop 对象,把旧 runloop 中需要的 mode copy 出来,放在新的 runloop 中
CFRunLoopCopyCurrentMode/CFRunLoopCopyAllModes
RunLoop 中的sources 和 observers
run_loop_structure
在 RunLoop 对象结构中有个 Mode,是为了在某种模式下 RunLoop 才会处理,可以减少每次需要监听的事件源
Sources 事件源
Input Sources
source分为两类,source0和source1。
- source0只含有回调指针,处理如UIEvent,CFSocket这类事件。它只能手动唤醒。
- source1则是有一个mach port和回调指针,能被Mach内核传递的信息唤醒。
- 触摸事件其实是source1接收系统事件后在回调 __IOHIDEventSystemClientQueueCallback
- __IOHIDEventSystemClientQueueCallback() 内触发的 Source0
- Source0 再触发的 _UIApplicationHandleEventQueue()
输入源:将事件以 asynchronous
的方式向 thread 发送 event
输入源 | Port-Based Sources | Custom Input Sources |
---|---|---|
对应 mode 结构体 | source1 | source0 |
消息从何来(发送方) | kernel 线程 | 其他用户thread |
监听事件 | Mach ports(内核 ports)event | 用户自定义Input sources 的event |
添加方法 | NSPort 对象 | CFRunLoopSourceRef |
Timer Sources
时间源:在预设时间后以同步的方式把 event 传递给 thread
Timer:是线程通知自己做某事的一种方式
Run Loop Observers 观察者
不同于 source 在同步或异步事件发生时触发,observer 会在 runloop 运行期间的某些特殊地方触发。
CFRunLoopObserverRef是 runloop 状态的观察者,能够监听RunLoop所有的状态改变。
- 进入 run loop
- 当 run loop 即将处理一个 timer
- 当 run loop 即将处理一个 input source
- 当 run loop 即将休眠
- 当 run loop 已经被唤醒,但在它处理唤醒它的事件之前
- 退出 run loop
对应 CFRunLoopActivity 枚举
1 | typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { |
RunLoop 核心对象的源码结构
RunLoop
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 struct __CFRunLoop {
CFRuntimeBase _base;
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
// CFSetAddValue(rl->_commonModeItems, rls); CFRunLoopSourceRef rlo
// CFSetAddValue(rl->_commonModeItems, rlo); CFRunLoopObserverRef rlo
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
RunLoopMode
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 struct __CFRunLoopMode {
CFStringRef _name;
Boolean _stopped;
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
CFMutableDictionaryRef _portToV1SourceMap;
__CFPortSet _portSet;
CFIndex _observerMask;
dispatch_source_t _timerSource;
dispatch_queue_t _queue;
Boolean _timerFired; // set to true by the source when a timer has fired
Boolean _dispatchTimerArmed;
mach_port_t _timerPort;
Boolean _mkTimerArmed;
};
RunLoopSource
1
2
3
4
5
6
7
8
9
10 struct __CFRunLoopSource {
CFRuntimeBase _base;
uint32_t _bits;
CFIndex _order; /* immutable */
CFMutableBagRef _runLoops;
union {
CFRunLoopSourceContext version0; /* immutable, except invalidation */
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
} _context;
};
RunLoop Observers
1
2
3
4
5
6
7
8
9 struct __CFRunLoopObserver {
CFRuntimeBase _base;
CFRunLoopRef _runLoop;
CFIndex _rlCount;
CFOptionFlags _activities; /* immutable */
CFIndex _order; /* immutable */
CFRunLoopObserverCallBack _callout; /* immutable */
CFRunLoopObserverContext _context; /* immutable, except invalidation */
};
RunLoop Timer
1
2
3
4
5
6
7
8
9
10
11
12
13
struct __CFRunLoopTimer {
CFRuntimeBase _base;
CFRunLoopRef _runLoop;
CFMutableSetRef _rlModes;
CFAbsoluteTime _nextFireDate;
CFTimeInterval _interval; /* immutable */
CFTimeInterval _tolerance; /* mutable */
uint64_t _fireTSR; /* TSR units */
CFIndex _order; /* immutable */
CFRunLoopTimerCallBack _callout; /* immutable */
CFRunLoopTimerContext _context; /* immutable, except invalidation */
};
使用到 RunLoop 的相关实践
先分析下系统中都使用 runLoop 做了什么
系统如何使用 RunLoop
app 运行时,打印CFRunLoop 内部样
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 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}}
// Obvserver
// Entry
CFRunLoopObserver {order = -2147483647, activities = 0x1, callout = _wrapRunLoopWithAutoreleasePoolHandler}
// BeforeWaiting
CFRunLoopObserver {order = 0, activities = 0x20, callout = _UIGestureRecognizerUpdateObserver}
// BeforeWaiting | Exit
CFRunLoopObserver {order = 1999000, activities = 0xa0, callout = _afterCACommitHandler}
// BeforeWaiting | Exit
CFRunLoopObserver {order = 2000000, activities = 0xa0, callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv}
// BeforeWaiting | Exit
CFRunLoopObserver {order = 2147483647, activities = 0xa0, 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)}
}// end commons,
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),
}
}
}
打开上面代码可以看到,系统默认注册了5个Mode:
- Observer:
1 | //内存管理相关: |
1 | //界面刷新相关: |
1 | //手势检测回调:手势变化时都会被这个观察者捕获。mach_msg_trap状态时也需要被RunLoop唤醒以后处理。 |
- Source:
1 | _handleHIDEventFetcherDrain: //释放IOHIDEvent对象的callback(source0),所以有IOHIDEvent事件的位置(通常是唤醒RunLoop的位置)都会有这个回调方法。 |
AutoreleasePool
根据上述分析,通过 RunLoop Observer,监听 RunLoop 状态来管理 AutoreleasePool的。
苹果在主线程 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 | _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv() |
定时器
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
实际上 RunLoop 底层也会用到 GCD 的东西,比如 RunLoop 是用 dispatch_source_t 实现的 Timer(评论中有人提醒,NSTimer 是用了 XNU 内核的 mk_timer,我也仔细调试了一下,发现 NSTimer 确实是由 mk_timer 驱动,而非 GCD 驱动的)。但同时 GCD 提供的某些接口也用到了 RunLoop, 例如 dispatch_async()。
当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。
关于网络请求
iOS 中,关于网络请求的接口自下至上有如下几层:
1 | CFSocket |
• CFSocket 是最底层的接口,只负责 socket 通信。
• CFNetwork 是基于 CFSocket 等接口的上层封装,ASIHttpRequest 工作于这一层。
• NSURLConnection 是基于 CFNetwork 的更高层的封装,提供面向对象的接口,AFNetworking 工作于这一层。
• NSURLSession 是 iOS7 中新增的接口,表面上是和 NSURLConnection 并列的,但底层仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 线程),AFNetworking2 和 Alamofire 工作于这一层。
下面主要介绍下 NSURLConnection 的工作过程。
通常使用 NSURLConnection 时,你会传入一个 Delegate,当调用了 [connection start] 后,这个 Delegate 就会不停收到事件回调。实际上,start 这个函数的内部会会获取 CurrentRunLoop,然后在其中的 DefaultMode 添加了4个 Source0 (即需要手动触发的Source)。CFMultiplexerSource 是负责各种 Delegate 回调的,CFHTTPCookieStorage 是处理各种 Cookie 的。
当开始网络传输时,我们可以看到 NSURLConnection 创建了两个新线程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader 这个线程内部会使用 RunLoop 来接收底层 socket 的事件,并通过之前添加的 Source0 通知到上层的 Delegate。
NSURLConnectionLoader 中的 RunLoop 通过一些基于 mach port 的 Source 接收来自底层 CFSocket 的通知。当收到通知后,其会在合适的时机向 CFMultiplexerSource 等 Source0 发送通知,同时唤醒 Delegate 线程的 RunLoop 来让其处理这些通知。CFMultiplexerSource 会在 Delegate 线程的 RunLoop 对 Delegate 执行实际的回调。
引用
Threading Programming Guide – Run Loops
CFRunLoopAddCommonMode
深入理解RunLoop 本文实例使用到 RunLoop 的相关实践 大量参考该文章,敬佩~
Event loop
后端eventloop
Event-driven programming
Threading Programming Guide(2)