RunLoop 原理分析

文章目录
  1. 1. 带着问题思考 Questions
  2. 2. RunLoop 来源
  3. 3. Event loop 机制
  4. 4. OSX/iOS 中的 Event Loop
    1. 4.1. RunLoop 特点:
  5. 5. Run Loop 内部执行流程
  6. 6. RunLoop底层实现
  7. 7. RunLoop结构
    1. 7.1. Mode
  8. 8. RunLoop 中的sources 和 observers
    1. 8.1. Sources 事件源
      1. 8.1.1. Input Sources
      2. 8.1.2. Timer Sources
    2. 8.2. Run Loop Observers 观察者
  9. 9. RunLoop 核心对象的源码结构
  10. 10. 使用到 RunLoop 的相关实践
    1. 10.1. 系统如何使用 RunLoop
    2. 10.2. AutoreleasePool
    3. 10.3. 事件响应
    4. 10.4. 手势识别
    5. 10.5. 界面更新
    6. 10.6. 定时器
    7. 10.7. PerformSelecter
    8. 10.8. 关于GCD
    9. 10.9. 关于网络请求
  11. 11. 引用

runloop_title

带着问题思考 Questions

  1. RunLoop 他是什么?
  2. RunLoop 结构是什么样的?
  3. 为什么要是这样的结构,其他的样子不可以吗?
  4. RunLoop 内部分析?
  5. 系统中有哪些功能使用了 runloop?
  6. 开发中如何使用 RunLoop

引用 CFRunLoopRef 源码:CF-1153.18.tar.gz

RunLoop 来源

  • 一般来说,thread 只运行一次,然后终止. 像命令行驱动程序。但是这样的程序没有用户交互性
  • 为了使程序增加用户交互性,就需要一个机制让程序不退出等待用户操作。使用Event Loop设计模式,让 thread 关联一个 event loop,监听 event,然后调用事件的 reactor 处理事件通常是交由 event loop 关联的 thread 处理。这样使 thread 保活
1
2
3
4
5
6
7
function main
initialize()
while message != quit
message := get_next_message()
process_message(message)
end while
end function

而对于没有 runloop 的thread

1
2
3
4
5
6
7
8
9
10
11
12
@objc
func task2() {
print(#function)
}

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
let thread = Thread {
print("task1")
}
thread.start()
perform(#selector(LongLiveViewController.task2), on: thread, with: nil, waitUntilDone: false)
}

结果只会打印 task1,因为 thread 没有 runloop,所以执行完当前任务 task1 以后会自动退出

1
2
3
4
5
6
7
8
9
func withoutRunloop() {
let thread = Thread {
print("task 1")
RunLoop.current.add(Port(), forMode: .default)
RunLoop.current.run(mode: .default, before: Date.distantFuture)
}
thread.start()
perform(#selector(LongLiveViewController.task2), on: thread, with: nil, waitUntilDone: true)
}

打印 task1,task2(),只有当 Port 从 runloop 中移除这个线程就会退出,否则他的生命周期由 runloop 维护着,具体看下文

Event loop 机制

Event loop 机制

Event Loop 在很多平台上都有,无论是 GUI,开始系统事件都需要 Event Loop 模式来处理交互程序

环境Event_Loop对应名称
OSX/iOSRunLoop
Windows消息队列
Linuxepoll,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, 直接返回。

  1. 通知 observer 已经进入 run loop (kCFRunLoopEntry)
  2. 通知 observer 有 timer 将要触发 (kCFRunLoopBeforeTimers)
  3. 通知 observer 有非基于端口的 input source(source0) 将要触发 (kCFRunLoopBeforeSources)
  4. 触发所有已就绪的非基于端口的 input source(source0)
  5. 如果一个基于端口的 input source(source1, 系统触发) 已就绪并等待触发,立即处理事件,并转至第 9 步
  6. 通知 observer 线程即将休眠 (kCFRunLoopBeforeWaiting)
  7. 让线程休眠,直到被以下条件唤醒:
    1. 有基于端口的 input source 事件到达
    2. timer 触发
    3. run loop 设定的超时时间到了
    4. run loop 被手动唤醒
  8. 通知 observer 线程刚刚被唤醒 (kCFRunLoopAfterWaiting)
  9. 处理待决事件
    1. 如果用户定义的 timer 触发了,处理 timer 事件并重启 run loop,跳回到第 2 步
    2. 如果 input source 触发了,分发事件
    3. 如果 run loop 被唤醒且没有超时,重启 run loop,跳回到第 2 步
  10. 通知 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

RunLoop底层实现

核心是源码中的第 7 步:
7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒

  • 一个基于 port 的Source 的事件。
  • 一个 Timer 到时间了
  • RunLoop 自身的超时时间到了
  • 被其他什么调用者手动唤醒

mach_msg() 函数实际上是调用了一个 Mach 陷阱 (trap),即函数mach_msg_trap(),陷阱这个概念在 Mach 中等同于系统调用。当你在用户态调用 mach_msg_trap() 时会触发陷阱机制,切换到内核态;内核态中内核实现的 mach_msg() 函数会完成实际的工作,如下图:

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set 自定义的 input source,由其他 thread 发送消息
CFMutableSetRef _sources1; // Set 基于 port 包含了一个 mach_port 和一个回调(函数指针),用于通过内核和其他线程相互发送消息
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};

typedef struct __CFRunLoop *CFRunLoopRef;
struct __CFRunLoop {
pthread thread;
CFMutableSetRef _commonModes; // Set 通用 modes集合,任何Source/Observer/Timer 加入其中以后,_commonModes 中的每个 mode 都会引用这些(Source/Observer/Timer ),并对其做相应处理
CFMutableSetRef _commonModeItems; // Set 所有_commonModes中引用 <Source/Observer/Timer>
CFRunLoopModeRef _currentMode; // Current Runloop Mode CFRunLoopCopyAllModes方法会得到它
...
};

run_loop_structure

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名称描述
DefaultNSDefaultRunLoopMode (Cocoa), kCFRunLoopDefaultMode (Core Foundation)App的默认运行模式,通常主线程是在这个运行模式下运行
ConnectionNSConnectionReplyMode (Cocoa)Cocoa 中结合 NSConnection 使用,用于监听回复(Reply),极少用到。(已弃用)
ModalNSModalPanelRunLoopMode (Cocoa)Cocoa 中 modal panel 使用它接收与之相关 Source 的事件
Event trackingNSEventTrackingRunLoopMode (Cocoa), UITrackingRunLoopMode (Cocoa Touch)跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响)
Common modesNSRunLoopCommonModes (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。

  1. source0只含有回调指针,处理如UIEvent,CFSocket这类事件。它只能手动唤醒。
  2. source1则是有一个mach port和回调指针,能被Mach内核传递的信息唤醒。
    1. 触摸事件其实是source1接收系统事件后在回调 __IOHIDEventSystemClientQueueCallback
    2. __IOHIDEventSystemClientQueueCallback() 内触发的 Source0
    3. Source0 再触发的 _UIApplicationHandleEventQueue()

输入源:将事件以 asynchronous 的方式向 thread 发送 event

输入源Port-Based SourcesCustom Input Sources
对应 mode 结构体source1source0
消息从何来(发送方)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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
// 即将进入Loop
kCFRunLoopEntry = (1UL << 0),
// 即将处理Timer
kCFRunLoopBeforeTimers = (1UL << 1),
// 即将处理Source
kCFRunLoopBeforeSources = (1UL << 2),
// 即将进入休眠
kCFRunLoopBeforeWaiting = (1UL << 5),
// 刚从休眠中唤醒
kCFRunLoopAfterWaiting = (1UL << 6),
// 即将退出Loop
kCFRunLoopExit = (1UL << 7),
// 所有状态
kCFRunLoopAllActivities = 0x0FFFFFFFU
};

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;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
dispatch_source_t _timerSource;
dispatch_queue_t _queue;
Boolean _timerFired; // set to true by the source when a timer has fired
Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
mach_port_t _timerPort;
Boolean _mkTimerArmed;
#endif
};
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:

  1. Observer:
1
2
3
//内存管理相关:
_wrapRunLoopWithAutoreleasePoolHandler: //AutoReleasePool最高优先级处理 (observer)
_wrapRunLoopWithAutoreleasePoolHandler: //AutoReleasePool最低优先级处理 (observer)
1
2
3
//界面刷新相关:
afterCACommitHandler: //监听CATransaction,刷新UI(observer), 优先级很低
_beforeCACommitHandler: //监听CATransaction
1
2
//手势检测回调:手势变化时都会被这个观察者捕获。mach_msg_trap状态时也需要被RunLoop唤醒以后处理。
_UIGestureRecognizerUpdateObserver: //监听CATransaction,刷新UI(observer)
  1. Source:
1
2
_handleHIDEventFetcherDrain: //释放IOHIDEvent对象的callback(source0),所以有IOHIDEvent事件的位置(通常是唤醒RunLoop的位置)都会有这个回调方法。
_handleEventQueue: //用户事件回调(source0),一般的addTarget: action: forControlEvents:方法都会加在source0,并由_handleEventQueue执行。

AutoreleasePool

根据上述分析,通过 RunLoop Observer,监听 RunLoop 状态来管理 AutoreleasePool的。
苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。

  1. 第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
  2. 第二个 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

实际上 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
2
3
4
CFSocket
CFNetwork ->ASIHttpRequest
NSURLConnection ->AFNetworking
NSURLSession ->AFNetworking2, Alamofire

• 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。

RunLoop_network

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)