iOS深入学习 - RunLoop

RunLoop概念:

原文作者Blog地址点这里。
一般来讲一个线程只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时执行任务但不退出,代码逻辑通常如下:runloop处理事件的代码逻辑模拟(Event Loop)

1
2
3
4
5
6
7
function loop() {
initialize();
do {
var message = get_next_messsage();
process_message(message);
} while (message != quite);
}

注意:
实现这种模型的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立即被唤醒

1、OSX/iOS系统中提供了两个对象:NSRunLoop、CFRunLoopRef。
CFRunLoopRef是在CoreFoundation框架内的,它提供了纯C函数的API,所有这些API都是线程安全的。
NSRunLoop是基于CFRunLoopRef封装的,提供了面向对象的API,但是这些API不是线程安全的。

CFRunLoopRef源码

跨平台的CoreFoundation版本源码

RunLoop与线程的关系

iOS开发中可能遇到两个线程对象:pthread_tNSThread。过去苹果有份文档/tn/tn2028.html)标明了NSThread只是pthread_t的封装,但是那份文档已经失效了,现在它们也有可能都是直接包装自最底层的mach thread。苹果没有提供这两个对象相互转换的接口,但不管怎么样,可以肯定的是pthread_tNSThread是一一对应的。比如,可以通过pthread_main_thread_np()或者[NSThread mainThread]来获取主线程;也可以通过pthread_self()或者[NSThread currentThread]来获取当前线程。CFRunLoop基于pthread来管理的。

苹果不允许直接创建RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。这两个函数内部的逻辑大概如下:

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
//全局的Dictionary,key是pthread_t, value是CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
//访问loopsDic时的锁
static CFSpinLock_t loopsLock;
//获取一个pthread对应的Runloop
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
OSSpinLockLock(&loopsLock);

if (!loopsDic) {
//第一次进入时,初始化全局Dic,并先为主线程创建一个RunLoop。
loopsDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
}

///直接从Dictionary里获取
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread);

if (!loop) {
///取不到,创建一个
loop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, thread, loop);
///注册一个回调,当线程销毁时,顺便也销毁对应的RunLoop
_CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
}

OSSPinLockUnlock(&loopsLock);
return loop;
}

CFRunLoopRef CFRunLoopGetMain() {
return _CFRRunLoopGet(pthread_main_thread_np());
}

CFRunLoopRef CFRunLoopGetCurrent() {
return _CFRunLoopGet(pthread_self());
}

从上面的代码可以看出,线程和RunLoop之间是一一对应的,其关系是保存在一个全局的Dictionary里。线程刚创建时并没有RunLoop,如果你不主动获取,拿它一直不会有。RunLoop的创建是发生在第一次获取时,RunLoop的销毁是发生在线程结束时。你只能在一个线程的内部获取其RunLoop(主线程除外)

RunLoop对外的接口

在CoreFoundation里面关于RunLoop有5个类:

  1. CFRunLoopRef
  2. CFRunLoopModeRef
  3. CFRunLoopSourceRef
  4. CFRunLoopTimerRef
  5. CFRunLoopObserverRef

其中CFRunLoopModeRef类并没有对外暴露,只是通过CFRunLoopRef的接口进行了封装。他们的关系如下:

一个RunLoop包含若干个Mode,每个Mode又包含若干个Source/Timer/Observer。每次调用RunLoop主函数时,只能指定其中一个Mode,这个Mode被称作CurrentMode。如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入。这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响。

CFRunLoopSourceRef是事件产生的地方。Source有两个版本:Source0和Source1.

  • Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用
    CFRunLoopSourceSignal(source),将这个Source标记为待处理,然后手动调用CFRunLoopWakeUp(runloop)来唤醒RunLoop,让其处理这个事件。
  • Source1包含了一个mach_port和一个回调(函数指针),被用于通过内核和其它线程互相发送消息。这种Source能主动唤醒RunLoop的线程,其原理下面会讲到。

CFRunLoopTimerRef是基于时间的触发器, 它和NSTimer是toll_free bridged(对象桥接)的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到RunLoop时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒已执行那个回调。

CFRunLoopObserverRef是观察者,每个Observer都包含了一个回调(函数指针),当RunLoop的状态发生变化的时候,观察者就能通过回调接受这个变化。可以观测的时间点有以下几个:

1
2
3
4
5
6
7
8
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
}

上面的Source/Timer/Observer被统称为mode item, 一个item可以被同时加入多个mode。但是一个item被重复加入同一个mode时是不会有效果的。如果一个mode中一个item都没有,则RunLoop会直接退出,不会进入循环。

RunLoop的Mode

CFRunLoopMode和CFRunLoop的结构大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _source0;
CFMutableSetRef _source1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
...
}

struct __CFRunLoop {
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes;
}

这里有个概念叫“CommonModes”:一个Mode可以将自己标为“Common”属性(通过将其ModeName添加到RunLoop的“commonModes”中)。每当RunLoop的内容发生变化时,RunLoop都会自动将_commonModeItems里的Source/Timer/Observer同步到具有“Common”标记的所有Mode里。

应用场景举例:主线程的RunLoop里有两个预置的Mode:kCFRunLoopDefaultModeUITrackingRunLoopMode。这两个Mode都已经被标记为“Common”属性。DefaultMode是APP平时所处的状态,TrackingRunLoopMode是追踪ScrollView滑动时的状态。当你创建一个Timer并追加到DefaultModel时,Timer会得到重复回调,但此时滑动一个TableView时,RunLoop会将mode切换为TrackingRunLoopMode,这时Timer就不会被调用,并且也不会影响滑动操作。

有时你需要一个Timer,在两个Mode中都能得到回调,一种办法就是将这个Timer分别加入这两个Mode。还有一种方式,就是将Timer加入到顶层的RunLoop的”commonModeItems“中。”commonModeItems“被RunLoop自动更新到所有具有“Common”属性的Mode里去。

  • CFRunLoop对外暴露的管理Mode接口只有下面2个:
1
2
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);
  • Mode暴露的管理mode item 的接口有下面几个:
1
2
3
4
5
6
CFRunLoopAddSource(CFRunLoopRef r1, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef r1, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef r1, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef r1, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef r1, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef r1, CFRunLoopTimerRef timer, CFStringRef mode);

你只能通过modeName来操作内部的mode,当你传入一个新的mode name但RunLoop内部没有对应的mode时,RunLoop会自动帮你创建对应的CFRunLoopModeRef。对于一个RunLoop来说,其内部的mode只能增加不能删除。

苹果公开提供的 Mode 有两个:kCFRunLoopDefaultMode (NSDefaultRunLoopMode)UITrackingRunLoopMode,你可以用这两个 Mode Name 来操作其对应的 Mode。

同时苹果还提供了一个操作 Common 标记的字符串:kCFRunLoopCommonModes (NSRunLoopCommonModes),你可以用这个字符串来操作 Common Items,或标记一个 Mode 为 “Common”。使用时注意区分这个字符串和其他 mode name。

RunLoop的内部逻辑

根据苹果文档里的说明,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
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
///用DefaultMode启动
void CFRunLoopRun(void) {
CFRunLoopRunSpecific(CFRunLoopGetCurrent(), KCFRunLoopDefaultMode, 1.0e10, false);
}

///用指定的Mode启动, 允许设置RunLoop超时时间
int CFRunLoopInMode(CFString modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

///RunLoop的实现
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
//首先根据modeName找到对应的mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
//如果mode里没有Source/Timer/Observer,直接返回
if (__CFRunLoopModeIsEmpty(currentMode)) return;

//1、通知Observer: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 = __CFRunLoopDoSource0(runloop, currentMode, stopAfterHandle);
//执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);

//5、如果有Source1(基于port)处于ready状态,直接处理这个Source1然后跳转去处理消息
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatch, &msg);
if (hasMsg) {
goto handle_msg;
}
}

//6、通知Observers: RunLoop的线程即将进入休眠(sleep)
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}

//7、调用mach_msg等待接受mach_port的消息。线程即将进入休眠,直接被下面某一个事件唤醒
//一个基于port的Source事件
//一个Timer到时间了
//RunLoop自身超时时间到了
//被其他什么调用者手动唤醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buff), &livePort) {
mach_msg(msg, MACH_RCV_MSG, port); //线程等待接收消息
}

//8、通知Observers: RunLoop的线程刚刚被唤醒
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
//收到消息,处理消息
handle_msg:

if (msg_is_timer) {
//9.1 如果一个timer时间到了,触发这个timer的回调
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time());
} else if(msg_is_dispatch){
//9.2 如果有dispatch到main_queue的block, 执行block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else {
//9.3 如果一个Source1(基于port)发出事件了,处理这个事件
CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
sourceHandledThisLoop = __CFRunLoopSource1(runloop, current, 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 = kCFRunLoopRunTimeOut;
} else if (__CFRunLoopIsStopped(runloop)) {
//被外部调用者强制停止了
retVal = kCFRunLoopRunStopped;
} else if (__kCFRunLoopModeIsEmpty(runloop, currentMode)) {
retVal = kCFRunLoopRunFinished;
}

//如果没超时, mode里没空,loop没有被停止, 那继续loop
} while (retVal == 0);
}

//10、通知Observers: RunLoop即将退出
__CFRunLoopDoObservers(r1, currentMode, kCFRunLoopExit);
}

可以看到,实际上RunLoop就是这样一个函数,其内部是一个do-while循环。当你调用CFRunLoopRun()时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。

RunLoop的底层实现

从上面代码可以看到,RunLoop的核心是基于mach port的,其进入休眠时调用的函数是mach_msg()。为了解释这个逻辑,下面稍微介绍一下OSX/iOS的系统架构。

苹果官方将整个系统大致划分为上述4个层次:
应用层包含用户能接触到的图形应用,例如:Spotlight、Aqua、SpringBoard等。
应用框架层即开发人员接触到的Cocoa等框架。
核心框架层包含各种核心框架、OpenGL等内容。
Darwin即操作系统的核心,包含系统内核、驱动、Shell等内容,这一层是开源的,其所有源码都可以在opensource.apple.com里找到。

我们在深入看一下Darwin这个核心的架构:

其中,在硬件层上面的三个组成部分:Mach、BSD、IOKit(还包括一些上面没标注的内容),共同组成了XNU内核。

  • XNU内核的内环被称作Mach,其作为一个微内核,仅提供了诸如处理器调度、IPC(进程间通信)等非常少量的基础服务。
  • BSD层可以看作围绕Mach层的一个外环,其提供了诸如进程管理、文件系统和网络等功能。
  • IOKit层为设备驱动提供了一个面向对象(C++)的一个框架。

Mach 本身提供的API非常有限,而且苹果也不鼓励使用Mach的API,但是这些API非常基础,如果没有这些API的话,其他任何工作都无法实施。在Mach中,所有的东西都是通过自己的对象实现的,进程、线程和虚拟内存都被称为“对象”。和其他架构不同,Mach的对象间不能直接调用,只能通过消息传递的方式实现对象间的通信。“消息”是Mach中最基础的概念,消息在两个端口(port)之间传递,这就是Mach的IPC(进程通信)的核心。

Mach的消息定义是在<mach/message.h>头文件中,很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct {
mach_msg_header_t header;
mach_msg_body_t body;
} mach_msg_base_t;

typedef struct {
mach_msg_bits_t msgh_bits;
mach_msg_size_t msgh_size;
mach_port_t msgh_remote_port;
mach_port_t msgh_local_port;
mach_port_name_t msgh_voucher_port;
mach_msg_id_t msgh_id;
} mach_msg_header_t;

一条Mach消息实际上就是一个二进制数据包(BLOB),其头部定义了当前端口local_port和目标端口remote_port,发送和接收消息是通过同一个API进行的,其option标记了消息传递的方向:

1
2
3
4
5
6
7
8
mach_msg_return_t mach_msg(
mach_msg_header_t *msg,
mach_msg_option_t *option,
mach_msg_size_t send_size,
mach_msg_size_t rcv_size,
mach_port_name_t rcv_name,
mach_msg_timeout_t timeout,
mach_port_name_t notify);

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

这些概念可以参考维基百科:System_callTrap_(computing))。

RunLoop 的核心就是一个 mach_msg() (见上面代码的第7步),RunLoop调用这个函数去接收消息, 如果没有别人发送 port 消息过来,内核会将线程置于等待状态。例如你在模拟器里跑起一个iOS的App,然后App静止时点击暂停,你会看到主线程调用栈是停留在mach_msg_trap() 这个地方。

关于具体的如何利用mach_port 发送消息,可以看看NSHipster这一篇文章,或者这里的中文翻译。

关于Mach的历史可以看看这篇很有趣的文章:Mac OS X 背后的故事(三) Mach 之父 Avie Tevanian

苹果用 RunLoop 实现的功能

首先我们可以看一下 App 启动后 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
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
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
}
}

// Observer
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),
}
}
}

可以看到系统默认注册了5个Mode:

  1. kCFRunLoopDefaultMode: App的默认Mode,通常主线程是在这个Mode下运行的。
  2. UITrackingRunLoopMode: 界面跟踪Mode,用于scrollView追踪触摸滑动,保证界面滑动时不受其他mode影响。
  3. UIInitializationRunLoopMode: 在刚启动App时进入的第一个Mode,启动完成后就不在使用。
  4. GSEventReceiveRunLoopMode: 接收系统事件的内部Mode,通常用不到
  5. kCFRunLoopCommonModes: 这是一个占位的Mode,没有实际作用

你可以在这里看到更多的苹果内部的Mode,但那些Mode在开发中就很难遇到了。

当RunLoop进行回调时,一般都是通过一个很长的函数调用出去(call out),当你在你的代码中下断点调试时,通常能在调用栈上看到这些函数。下面是这几个函数的整理版本,如果你在调用栈上看到这些长函数名,在这里查找一下就能定位到具体的调用地点了:

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
{
///1、通知Observer,即将进入runloop
///此处有Observer会创建AutoreleasePool:_objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry)
do {
///2、通知Observers:即将触发timer回调
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNTION__(kCFRunLoopBeforeTimers)
/// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);

/// 4. 触发 Source0 (非基于port的) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);

/// 5. 通知Observers,即将进入休眠
/// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);

/// 6. sleep to wait msg.
mach_msg() -> mach_msg_trap();

/// 7. 通知Observers,线程被唤醒
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);

/// 8.1 如果是被Timer唤醒的,回调Timer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);

/// 8.2 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);

/// 8.3 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
} while (...);
/// 9. 通知Observers,即将退出RunLoop
/// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}

AutoreleasePool

App启动后,苹果会在主线程RunLoop里注册两个 Observer,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()

第一个Observer监视的事件是Entry(即将进入RunLoop),其回调内会调用_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/CALayersetNeedsLayout/setNeedsDisplay 方法后,这个 UIView/CALayer 方法后,这个 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的东西,(NSTimer是用了XNU内核的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工作于这一层。
  • NSURLSessioniOS7中新增的借口,表面上是和NSURLConnection并列的,但底层仍然用到了NSURLConnection的部分功能(比如com.apple.NEURLConnectionLoader线程),AFNetworking2Alamofire工作于这一层。

下面主要介绍下NSURLConnnection的工作过程。

通常使用NSURLConnection时,你会传入一个Delegate,当调用了[connection start]后,这个Delegate就会不停收到事件回调。实际上,start这个函数的内部会获取CurrentRunLoop,然后在其中的DefaultMode添加了4个Source0(即需要手动触发的Source)。CFMultiplexerSource是负责各种Delegate回调的,CFHTTPCookieStorage是处理各种Cookie的。

当开始网络传输时,我们可以看到NSURLConnection创建了两个新线程:com.apple.NSURLConnectionLoadercom.apple.CFSocket.private。其中CFSocket线程是处理底层socket连接的。NSURLConnectionLoader这个线程内部会使用RunLoop来接收底层socket事件,并通过之前添加的Source0通知上层的Delegate

NSURLConnectionLoader中的RunLoop通过一些基于mach portSource接收来自底层CFSocket的通知。当收到通知后,其会在合适的时机向CFMultiplexerSourceSource0发送通知,同时唤醒Delegate线程的RunLoop来让其处理这些通知。CFMultiplexerSource会在Delegate线程的RunLoopDelegate执行实际的回调。

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
19
+(void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
[runloop addPort:[NSMachPort port] forMode: NSDefaultRunLoopMode];
[runloop run];
}
}

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

return _networkingRequestThread;
}

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,监听了kCFRunLoopBeforeWaitingkCFRunLoopExit事件,在收到回调时,遍历所有之前放入到队列的待处理的任务,然后一一执行。

Powered by Hexo and Hexo-theme-hiker

Copyright © 2013 - 2020 KNOWLEDGE IS POWER All Rights Reserved.

访客数 : | 访问量 :