探索RunLoop 

为什么要用RunLoop

在深入地了解怎样用RunLoop和它的内部实现之前,先来看一下为什么需要RunLoop。我们知道一个线程在干完交给它的任务后,该线程就运行结束了,你无法在后面再让它接着处理任务了。所以如果我们需要线程过一段时间能来处理一下任务,就要想办法让线程一直活跃,至少在需要它处理任务时能唤醒它。最简单的办法就是在它的入口方法中用一个while循环一直循环着,直到遇到我们设置的终止条件。但是这样做非常地浪费CPU资源,如果能让线程在需要的时候活跃,在不需要的时候就休眠,就很棒了。所以RunLoop来了,将它放到线程中,然后再用它管理要持续处理的任务,就能充分利用线程了。举个栗子,在线程中启动一个定时器,可以看到,如果不采取措施,定时器的任务永远无法得到执行。

1
2
3
4
5
6
7
8
9
10
11
12
NSThread *workThread = [[NSThread alloc] initWithTarget:self selector:@selector(threadMain) object:nil];
[workThread start];

- (void)threadMain
{
[NSTimer scheduledTimerWithTimeInterval:0.1 target:self
selector:@selector(doFireTimer:) userInfo:nil repeats:YES];
}

- (void)doFireTimer:(id)data {
NSLog(@"timer fired.");
}

运行后会发现控制台永远都不会有输出,因为在threadMain方法返回后,线程就退出了。

我们的App在启动后,在无事可做时还能不退出运行,就是因为在App一启动时主线程的RunLoop也启动了。RunLoop的目的就是在有任务时让线程保持活跃,在没有时让线程进入休眠。Cocoa和Core Foundation都提供了RunLoop对象,NSRunLoopCFRunLoop。可以通过NSRunLoopgetCFRunLoop方法得到对应的CFRunLoopRef,并不能直接通过toll-free bridging转换。

NSRunLoop不是线程安全的,不要在其他线程调用NSRunLoop的方法,否则可能会导致意想不到的结果。但CFRunLoop是线程安全的。

下一节通过CFRunLoop源码,我们再进一步理解RunLoop。

阅读CFRunLoop的源码时有一个小tips,因为CFRunLoop是线程安全的,所以在函数中基本上都能看到lock和unclock的代码,但是这部分代码与功能实现逻辑无关,可以把lock相关的代码都去掉避免干扰。在官网下载下来的代码的大括号对齐方式不够清晰,我们可以把函数拷贝到一个空的Objective-C项目中,Xcode会自动调整大括号的缩进。此时再看会比较易看。

RunLoop的构成

RunLoop不会主动去揽事情做,你要告诉它啥时候要做什么事。目前能添加到RunLoop中的对象类型有3种:input sources(CFRunLoopSource)、timer sources(CFRunLoopTimer)、observers(CFRunLoopObserver)。

input sources负责异步地将事件传递到你的线程。通常有两种input sources,一种是基于端口的(port-based)input sources,它会监听应用的Mach端口。port-based sources是被内核自动打标记的,打标记之后该sources就可以准备开始干活了。另外一种是自定义的input sources,开发者要在其他线程手动调用方法对其打标记。

timer sources负责在将来以一个预设的时间同步地传递事件到你的线程。定时器是通知线程自己去做事情的一种方式。

sources是在有异步或同步的事件发生时开始干活,但是observers是RunLoop本身执行的过程中被触发干活的。你可以使用observers让线程准备好去处理收到的事件,或者在线程进入休眠之前处理一些事情。

那在RunLoop的一次运行过程中,是不是所有事件都照单全收,有就处理呢?不是的,RunLoop还有一个对象叫运行模式(RunLoop Modes)。在我们添加sources或observers时都要指定是添加到哪个或哪些模式中。在启动RunLoop时也要指定以哪个模式运行。

Core Foundation中有关RunLoop的类有如下4个:

1
2
3
4
typedef struct __CFRunLoop *         CFRunLoopRef;
typedef struct __CFRunLoopSource * CFRunLoopSourceRef;
typedef struct __CFRunLoopObserver * CFRunLoopObserverRef;
typedef struct __CFRunLoopTimer * CFRunLoopTimerRef;

但是在CFRunLoop.c中,我们还能找到一个与RunLoop Mode相关的类:

1
typedef struct __CFRunLoopMode *     CFRunLoopModeRef;

其中CFRunLoopRefCFRunLoopModeRef的结构大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef struct __CFRunLoop * CFRunLoopRef;

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

struct __CFRunLoopMode {
CFStringRef _name; // Mode Name,例如@"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
......
};

由上面的代码可以看到,一个RunLoop中包含多个Mode,一个Mode中又包含多个Source/Timer/Observer。

CFRunLoopModeRef

官方文档中关于RunLoop Mode的有如下介绍:

A run loop mode is a collection of input sources and timers to be monitored and a collection of run loop observers to be notified.

You must be sure to add one or more input sources, timers, or run-loop observers to any modes you create for them to be useful.

You use modes to filter out events from unwanted sources during a particular pass through your run loop.

一个RunLoop Mode是Source/Timer/Observer的集合。在运行一个RunLoop时必须要指定一个Mode,在运行的过程中,只有进入到这个Mode中的Source/Timer/Observer才会得到处理。这个Mode叫做currentMode,可以通过[NSRunLoop currentRunLoop].currentMode得到currentMode。这些Source/Timer/Observer又叫做Mode Items

这样我们就能使用Mode过滤掉在一次RunLoop运行中不想处理的Sources了。因此我们也必须把Source/Timer/Observer添加到某个Mode中,它才有机会得到处理。

我们在使用Mode的地方只要指定Mode Name就可以了,它是CFStringRef类型,我们用一个字符串就能自定义一个Mode。Core Foundation对外提供了两个Mode:kCFRunLoopDefaultModekCFRunLoopCommonModes,在Cocoa中对应的是NSDefaultRunLoopModeNSRunLoopCommonModes,此外Cocoa中还有一个对外的Mode是UITrackingRunLoopMode。当然了,Cocoa和Core Foundation中的Mode远不止这些,像一些系统内部用的私有Mode,我们平时开发中基本用不上,暂且不管了。

  • kCFRunLoopDefaultMode/NSDefaultRunLoopMode:处理除了NSConnection对象外的input sources的模式。
  • UITrackingRunLoopMode:控件中发生追踪事件时用的模式。

kCFRunLoopCommonModes并不是一个真正的Mode,从上面的代码也可以看到,它只是一个数组,里面会存其他的Mode,kCFRunLoopDefaultModeNSEventTrackingRunLoopMode都已经默认添加到了commonModes中。当我们把Source/Timer/Observer加入到kCFRunLoopCommonModes中,就相当于把它们加入到commonModes中的所有Mode中。

使用CFRunLoopAddCommonMode方法将一个Mode加入到commonModes中,该方法的内部实现大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName) {
// 如果原来的commonModes中没有该modeName
// 将之前的commonModeItems复制一份,以待一会使用
// 将新的modeName添加到commonModes
// 再将之前的commonModeItems同步到新加入的mode中
if (!CFSetContainsValue(rl->_commonModes, modeName)) {
CFSetRef set = rl->_commonModeItems ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModeItems) : NULL;
CFSetAddValue(rl->_commonModes, modeName);
if (NULL != set) {
CFTypeRef context[2] = {rl, modeName};
/* add all common-modes items to new mode */
CFSetApplyFunction(set, (__CFRunLoopAddItemsToCommonMode), (void *)context);
CFRelease(set);
}
} else {
}
}

Core Foundation中对外提供的管理Mode Item的接口有下面几个:

1
2
3
4
5
6
CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef mode);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef mode);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef mode);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef mode);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);

在添加Mode Item方法中,都可以找到一下面这样一行代码:

1
2
// 在没有找到Mode时,会创建一个返回。
CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true);

也就是说,在添加Mode Item时,如果此时Mode还没有,这些方法就会给创建一个,所以并不需要开发者自己创建Mode。

Core Foundation对外提供了以下两个方法运行一个RunLoop:

1
2
3
4
// 使用kCFRunLoopDefaultMode运行RunLoop
CFRunLoopRun(void);
// 使用指定的mode运行RunLoop,如果该mode中没有mode item,就会返回kCFRunLoopRunFinished以结束RunLoop的运行
CFRunLoopRunInMode(CFStringRef mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled);

CFRunLoopSourceRef

CFRunLoopSourceRef的大致结构如下:

1
2
3
4
5
6
7
struct __CFRunLoopSource {
......
union {
CFRunLoopSourceContext version0;
CFRunLoopSourceContext1 version1;
} _context;
};

Source是事件产生的地方,从上面的代码可以看到,Source有两个版本,Source0和Source1。CFRunLoopSourceRef用了不同的context来封装Source0和Source1所需的一些回调等信息。

Source0有公开的API可以供开发者调用,创建它时就需要传入上面所看到的CFRunLoopSourceContext,它的结构如下,其中的字段就是我们在创建它时需要传入的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct {
// 此处version只能取0
CFIndex version;
// 通常是传入指向Source自己的指针
void * info;
const void *(*retain)(const void *info);
void (*release)(const void *info);
CFStringRef (*copyDescription)(const void *info);
Boolean (*equal)(const void *info1, const void *info2);
CFHashCode (*hash)(const void *info);
// 当Source被加入到RunLoop中的回调
void (*schedule)(void *info, CFRunLoopRef rl, CFStringRef mode);
// 当Source被从RunLoop中移除时的回调
void (*cancel)(void *info, CFRunLoopRef rl, CFStringRef mode);
// 当Source被告知可以处理事件时的回调
void (*perform)(void *info);
} CFRunLoopSourceContext;

Source0不能主动触发事件,需要先调用CFRunLoopSourceSignal(),将这个Source标记为待处理,然后手动调用CFRunLoopWakeUp()来唤醒 RunLoop,让其处理这个事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
void CFRunLoopSourceSignal(CFRunLoopSourceRef rls) {
if (__CFIsValid(rls)) {
__CFRunLoopSourceSetSignaled(rls);
}
}

void CFRunLoopWakeUp(CFRunLoopRef rl) {
if (__CFRunLoopIsIgnoringWakeUps(rl)) {
return;
}

SetEvent(rl->_wakeUpPort);
}

Source1是基于Mach Port的,被用于通过内核和其他线程相互发送消息,能主动唤醒RunLoop的线程。

Mach处于iOS/OSX系统架构中的XNU内核中的内环,提供处理器调度、IPC(进程间通信)等非常少量的基础服务。Mach的对象间通信通过消息在两个端口(Port)之间传递来完成。Mach中发送和接收消息是通过同一个APImach_msg进行的,它的参数option指定了消息传递的方向。mach_msg函数中调用了另一个APImach_msg_trap,当在用户态调用它时,会触发陷阱机制,切换都内核状态。内核态中实现的mach_msg函数会完成实际的工作。

RunLoop就是调用mach_msg函数来收消息(下一节从代码可以看到),如果没有别人发送Port消息过来,内核会将线程置于等待状态。在模拟器中启动一个iOS应用,然后在其静止时点击暂停,会看到主线程调用栈停留在了mach_msg_trap函数这里。

CFRunLoopObserverRef

CFRunLoopObserverRef是观察者,把它加入到RunLoop中,并指定我们关心的节点,在RunLoop运行的过程中就会收到相应的通知。我们能够观察到的RunLoop的状态如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
// 进入RunLoop。
kCFRunLoopEntry = (1UL << 0),
// 即将处理timer。
kCFRunLoopBeforeTimers = (1UL << 1),
// 即将处理input sources。
kCFRunLoopBeforeSources = (1UL << 2),
// 即将进入休眠。
kCFRunLoopBeforeWaiting = (1UL << 5),
// RunLoop已经唤醒,但是还没有处理唤醒它的事件。
kCFRunLoopAfterWaiting = (1UL << 6),
// 退出RunLoop。
kCFRunLoopExit = (1UL << 7),
// 全部活动
kCFRunLoopAllActivities = 0x0FFFFFFFU
};

Core Foundation提供了两个API来创建CFRunLoopObserverRef

1
2
3
4
5
6
// activities可以用组合的方式,传入多个
// callout需要传入一个函数指针,该函数用来接收通知
CFRunLoopObserverCreate(CFAllocatorRef allocator, CFOptionFlags activities, Boolean repeats, CFIndex order, CFRunLoopObserverCallBack callout, CFRunLoopObserverContext *context);

// 从iOS5.0/OSX10.7开始CoreFoundation中支持block了,所以此处不是传入函数指针了,而是传入一个block
CFRunLoopObserverCreateWithHandler(CFAllocatorRef allocator, CFOptionFlags activities, Boolean repeats, CFIndex order, void (^block) (CFRunLoopObserverRef observer, CFRunLoopActivity activity))

CFRunLoopTimerRef

CFRunLoopTimerRef是基于时间的触发器,它和NSTimer是toll-free bridged的,可以混用。Timer负责在将来以一个预设的时间同步地传递事件到你的线程。定时器是通知线程自己去做事情的一种方式。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。

Core Foundation同样提供了两个API来创建CFRunLoopTimerRef,跟创建CFRunLoopObserverRef一样,一个可以传入函数指针,一个可以传入block。

1
2
3
CFRunLoopTimerRef CFRunLoopTimerCreate(CFAllocatorRef allocator, CFAbsoluteTime fireDate, CFTimeInterval interval, CFOptionFlags flags, CFIndex order, CFRunLoopTimerCallBack callout, CFRunLoopTimerContext *context);

CFRunLoopTimerRef CFRunLoopTimerCreateWithHandler(CFAllocatorRef allocator, CFAbsoluteTime fireDate, CFTimeInterval interval, CFOptionFlags flags, CFIndex order, void (^block) (CFRunLoopTimerRef timer));

RunLoop的运行逻辑

上面对RunLoop中的几个重要构成部分做了介绍,这一节我们继续深入到源码中,看看获取RunLoop的逻辑,处理追加到RunLoop中的各种Source的逻辑,以及对各种Source处理的逻辑。

获取RunLoop

我们不能直接通过API来创建一个RunLoop,Core Foundation中提供了以下两个API来获得当前线程的RunLoop,和主线程的RunLoop。

1
2
CFRunLoopRef CFRunLoopGetCurrent(void);
CFRunLoopRef CFRunLoopGetMain(void);

获取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
// 全局字典,用来存RunLoop,key为线程,value为对应的RunLoop
// 此处也可以看到线程和RunLoop是一一对应的
static CFMutableDictionaryRef __CFRunLoops = NULL;

CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
// 缓存RunLoop的字典为空
if (!__CFRunLoops) {
// 创建一个临时字典
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
// 创建mainLoop,将mainLoop以main thread为key存储到字典中
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
// 将dict中的key-value存储到全局字典_CFRunLoops中
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
}
// 尝试从全局字典_CFRunLoops中取出key为传入的线程对应的value
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
// 如果该线程对应的RunLoop还不存在,则创建一个,并存入到__CFRunLoops中
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
CFRelease(newLoop);
}
if (pthread_equal(t, pthread_self())) {
// 将loop缓存起来,_CFSetTSD这个函数在下面会再详细说一下
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
// 将RunLoop的销毁函数__CFFinalizeRunLoop缓存起来
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}

CFRunLoopRef CFRunLoopGetMain(void) {
// 因为main RunLoop只有一个,所以这里用static定义了,避免重复创建
static CFRunLoopRef __main = NULL; // no retain needed
if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
return __main;
}

CFRunLoopRef CFRunLoopGetCurrent(void) {
// 如果取到了RunLoop就直接返回
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}

把RunLoop的管理放到Core Foundation中,而不是交给开发者自己管理,严格地限制了线程和RunLoop的一一对应关系。线程刚创建时并没有RunLoop,如果不主动获取,它一直都不会有。RunLoop的创建是发生在第一次获取时,RunLoop的销毁是在线程退出时。

上面出现了_CFSetTSD函数可能会让有有点费解,在ForFoundationOnly.h中可以找到对它的定义:

1
2
3
4
5
6
7
// ---- Thread-specific data --------------------------------------------

// Get some thread specific data from a pre-assigned slot.
CF_EXPORT void *_CFGetTSD(uint32_t slot);

// Set some thread specific data in a pre-assigned slot. Don't pick a random value. Make sure you're using a slot that is unique. Pass in a destructor to free this data, or NULL if none is needed. Unlike pthread TSD, the destructor is per-thread.
CF_EXPORT void *_CFSetTSD(uint32_t slot, void *newVal, void (*destructor)(void *));

从注释中可以到,_CFGetTSD_CFSetTSD用来读和取线程相关的数据,TSD也就是Thread-specific data的缩写。

_CFGetTSD_CFSetTSD的实现在CFPlatform.c中可以找到,如下:

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
// For the use of CF and Foundation only
void *_CFGetTSD(uint32_t slot) {
if (slot > CF_TSD_MAX_SLOTS) {
_CFLogSimple(kCFLogLevelError, "Error: TSD slot %d out of range (get)", slot);
HALT;
}
__CFTSDTable *table = __CFTSDGetTable();
if (!table) {
// Someone is getting TSD during thread destruction. The table is gone, so we can't get any data anymore.
_CFLogSimple(kCFLogLevelWarning, "Warning: TSD slot %d retrieved but the thread data has already been torn down.", slot);
return NULL;
}
uintptr_t *slots = (uintptr_t *)(table->data);
return (void *)slots[slot];
}

// For the use of CF and Foundation only
void *_CFSetTSD(uint32_t slot, void *newVal, tsdDestructor destructor) {
if (slot > CF_TSD_MAX_SLOTS) {
_CFLogSimple(kCFLogLevelError, "Error: TSD slot %d out of range (set)", slot);
HALT;
}
__CFTSDTable *table = __CFTSDGetTable();
if (!table) {
// Someone is setting TSD during thread destruction. The table is gone, so we can't get any data anymore.
_CFLogSimple(kCFLogLevelWarning, "Warning: TSD slot %d set but the thread data has already been torn down.", slot);
return NULL;
}

void *oldVal = (void *)table->data[slot];

table->data[slot] = (uintptr_t)newVal;
table->destructors[slot] = destructor;

return oldVal;
}

从实现上可以看到,_CFGetTSD_CFSetTSD和操作的是一个__CFTSDTable结构,它的结构如下:

1
2
3
4
5
6
// Data structure to hold TSD data, cleanup functions for each
typedef struct __CFTSDTable {
uint32_t destructorCount;
uintptr_t data[CF_TSD_MAX_SLOTS];
tsdDestructor destructors[CF_TSD_MAX_SLOTS];
} __CFTSDTable;

所以_CFGetTSD干的事儿就是从table中取出data中slot位置存储的内容,_CFSetTSD干的事儿是把newVal存储到table的data中slot位置,把析构函数destructor存储到destructors中slot位置。destructor也可以传入NULL。slot的取值并不随机的,是Core Foundation中定义的一组枚举值。

1
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);

这里PTHREAD_DESTRUCTOR_ITERATIONS表示的是线程退出时,操作系统实现试图销毁线程私有数据的最大次数。CFPlatform.c中的__CFTSDFinalize函数会将table中的data中的数据置为NULL,执行destructor中的函数,从而销毁线程的私有数据。所以__CFFinalizeRunLoop函数就会在此处被调用。__CFFinalizeRunLoop函数主要做的事就是把加入到RunLoop中Source都移除,然后释放RunLoop。

运行RunLoop

咱们再来看一下RunLoop内部的运行逻辑。苹果官方文档中对其内部逻辑描述如下:

  1. Notify observers that the run loop has been entered.
  2. Notify observers that any ready timers are about to fire.
  3. Notify observers that any input sources that are not port based are about to fire.
  4. Fire any non-port-based input sources that are ready to fire.
  5. If a port-based input source is ready and waiting to fire, process the event immediately. Go to step 9.
  6. Notify observers that the thread is about to sleep.
  7. Put the thread to sleep until one of the following events occurs:
    • An event arrives for a port-based input source.
    • A timer fires.
    • The timeout value set for the run loop expires.
    • The run loop is explicitly woken up.
  8. Notify observers that the thread just woke up.
  9. Process the pending event.
    • If a user-defined timer fired, process the timer event and restart the loop. Go to step 2.
    • If an input source fired, deliver the event.
    • If the run loop was explicitly woken up but has not yet timed out, restart the loop. Go to step 2.
  10. Notify observers that the run loop has exited.

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
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
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
// 对外提供的API,处理当前mode中的items,只在被强制停止,或者items都处理完了后才会结束。
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

// 对外提供的API,可以指定处理哪个mode中的items,可以指定超时时间,和是否处理完事件就返回。
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}


SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */

// 在这里也可以看到,如果当前mode是为空,或者当前mode中没有要处理的items,就会直接返回了,不再往下进行。
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);

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
if (currentMode->_observerMask & kCFRunLoopEntry )
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);

// __CFRunLoopRun开始真正执行RunLoop的逻辑了
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);

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

__CFRunLoopPopPerRunData(rl, previousPerRun);
rl->_currentMode = previousMode;
return result;
}


static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
uint64_t startTSR = mach_absolute_time();

if (__CFRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
return kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
rlm->_stopped = false;
return kCFRunLoopRunStopped;
}

// 声明 dispatchPort 变量,作为一个 mach_port 通信的端口,初始化值为 MACH_PORT_NULL
mach_port_name_t dispatchPort = MACH_PORT_NULL;

// 检测是否在主线程 && ( (是队列发的消息&&mode为null)||(不是队列发的消息&&不在主队列))
Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));

// 如果是队列安全的,并且是主线程runloop,设置它对应的通信端口
if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) dispatchPort = _dispatch_get_main_queue_port_4CF();

// 设置判断是否为最后一次 dispatch 的端口通信的变量
Boolean didDispatchPortLastTime = true;
// retVal为0时while会一直循环
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是mach_port_t的别名;
__CFPortSet waitSet = rlm->_portSet;

// 不让 RunLoop 忽略唤醒消息
__CFRunLoopUnsetIgnoreWakeUps(rl);

// 2. 通知 Observers:RunLoop 即将触发 Timer 回调。
if (rlm->_observerMask & kCFRunLoopBeforeTimers)
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
// 3. 通知 Observers:RunLoop 即将触发 Source0 (非 mach port) 回调。
if (rlm->_observerMask & kCFRunLoopBeforeSources)
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
// 执行 block
__CFRunLoopDoBlocks(rl, rlm);
// 4. 执行 Source0 (非 mach port) 。
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);

if (sourceHandledThisLoop) {
// 执行完 Source0 后,假如还有需要执行的,再执行一次 block
__CFRunLoopDoBlocks(rl, rlm);
}

// poll 变量,是否处理 Source0 或未超时
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);

if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
msg = (mach_msg_header_t *)msg_buffer;

// 5. 如果有 Source (基于port) 处于 ready 状态
// 此处最后传入0,该方法会立即去轮询有没有端口收到了信息
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0)) {
// 跳转到handle_msg处理 Source (基于port)
goto handle_msg;
}
}

// port不是dispatchPort时,该值为false
didDispatchPortLastTime = false;

// 6.通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
// 如果实际没有处理 Source0 或者未超时,不会进入睡眠,所以不会通知
if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting))
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);

// 设置标志位, RunLoop 休眠
__CFRunLoopSetSleeping(rl);

// 使用 GCD 的话,将 GCD 端口加入监听端口集合中
__CFPortSetInsert(dispatchPort, waitSet);

msg = (mach_msg_header_t *)msg_buffer;

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

/**** 下面的代码会等到线程重新被唤醒时再执行,根据livePort来确定,是被哪种事件唤醒的 ***/

//对 waitSet 里的 dispatchPort 端口做移除
__CFPortSetRemove(dispatchPort, waitSet);

//让 Run Loop 忽略唤醒消息,因为已经重新在运行了
__CFRunLoopSetIgnoreWakeUps(rl);

// user callouts now OK again
__CFRunLoopUnsetSleeping(rl);

// 8. 通知 Observers:kCFRunLoopAfterWaiting, 线程刚被唤醒
// 如果实际没有处理 Source0 或者未超时,不会进入睡眠,所以不会通知
if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting))
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

// 处理对应唤醒的消息
handle_msg:;
if (MACH_PORT_NULL == livePort) {
CFRUNLOOP_WAKEUP_FOR_NOTHING();
// handle nothing
} else if (livePort == rl->_wakeUpPort) {
CFRUNLOOP_WAKEUP_FOR_WAKEUP();
// do nothing on Mac OS
}
else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
// 9.1 被 timers 唤醒,处理 timers
CFRUNLOOP_WAKEUP_FOR_TIMER();
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
// Re-arm the next timer
__CFArmNextTimerInMode(rlm, rl);
}
}
else if (livePort == dispatchPort) {
// 9.2 如果有dispatch到main_queue的block,执行block
CFRUNLOOP_WAKEUP_FOR_DISPATCH();
// 设置 CFTSDKeyIsInGCDMainQ 位置的 TSD 为 6
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);
// 处理 msg
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
// 设置 CFTSDKeyIsInGCDMainQ 位置的 TSD 为 0
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);

sourceHandledThisLoop = true;
didDispatchPortLastTime = true;
} else {
// 9.3 被 Source1 唤醒
CFRUNLOOP_WAKEUP_FOR_SOURCE();
// Despite the name, this works for windows handles as well
CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
if (rls) {
mach_msg_header_t *reply = NULL;
//处理 Source1 ,并返回执行结果
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
if (NULL != reply) {
// 发送reply消息(假如 reply 不为空)
(void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply);
}
}
}

// 执行加入到Loop的block
__CFRunLoopDoBlocks(rl, rlm);

// 根据一次循环后的状态,给 retVal 赋值 。状态不变则继续循环
if (sourceHandledThisLoop && stopAfterHandle) {
// 进入loop时参数说处理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
// 超出传入参数标记的超时时间了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
// 被外部调用者强制停止了
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
// mode被终止
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
// source/timer/observer一个都没有了
retVal = kCFRunLoopRunFinished;
}
} while (0 == retVal);

return retVal;
}

CFRunLoopRun函数中用了一个do-while,结合__CFRunLoopRun函数的退出条件,可以看出这是让RunLoop在超时后还能再次进入到RunLoop中。

__CFRunLoopRun函数中关键处是调用了__CFRunLoopServiceMachPort函数,该函数中又调用了mach_msg函数来接收mach_port的消息。关于mach_msg的说明在前面讲CFRunLoopSourceRef的小节中有说到。

1
2
3
4
5
6
7
// port 指定要监听的port
// buffer 放置收到消息的缓冲区
// buffer_size 缓冲区的大小
// livePort 告诉调用方当前收到的消息来自哪个port
// timeout 如果传入0,会立即去轮训有没有消息,如果传入TIMEOUT_INFINITY,会将让当前线程睡眠直到有消息过来。
// 返回是否收到了消息
static Boolean __CFRunLoopServiceMachPort(mach_port_name_t port, mach_msg_header_t **buffer, size_t buffer_size, mach_port_t *livePort, mach_msg_timeout_t timeout)

在这个函数中,作者还调皮地注释了一句《哈姆雷特》中的台词:In that sleep of death what nightmares may come …,好形象,哈哈

官方文档中描述的RunLoop的内部运行逻辑,只提到了何时处理source/timer/observer,但从上面的代码中,可以看到,整个的运行过程中还伴随着处理blockdispatch相关的事件。再次总结一下RunLoop的内部运行逻辑如下:

RunLoop是如何执行任务的

从上面的源码中,可以看到,在处理各种事件时,__CFRunLoopRun中调用了下面这些函数:

  • __CFRunLoopDoObservers
  • __CFRunLoopDoSources0
  • __CFRunLoopDoSource1
  • __CFRunLoopDoTimers
  • __CFRunLoopDoBlocks
  • __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__

__CFRunLoopDoObservers

1
static void __CFRunLoopDoObservers(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopActivity activity)

RunLoop在执行到通知Observers时会调用该API,找到在指定mode中的添加的所有Observers,然后调用__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__执行创建CFRunLoopObserverRef时绑定的回调函数。

__CFRunLoopDoSources0

1
static Boolean __CFRunLoopDoSources0(CFRunLoopRef rl, CFRunLoopModeRef rlm, Boolean stopAfterHandle)

RunLoop在执行到要处理Source0时会调用该API,该函数会找到指定mode中添加的所有Source0,然后依然是调用了一个函数名很长的call out函数__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__,来执行创建Source0时绑定的回调函数。

__CFRunLoopDoSource1

1
2
3
4
5
static Boolean __CFRunLoopDoSource1(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopSourceRef rls
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
, mach_msg_header_t *msg, CFIndex size, mach_msg_header_t **reply
#endif
)

RunLoop发现有基于mach_port的Source时会调用该API,将此Source传入。在该API的内部同样调用了一个call out函数__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__来执行回调函数。

__CFRunLoopDoTimers

1
static Boolean __CFRunLoopDoTimers(CFRunLoopRef rl, CFRunLoopModeRef rlm, uint64_t limitTSR)

开发者使用NSTimer相关API即可注册被执行的任务,RunLoop在执行到要处理Timers的任务时会调用该API,其内部依然是调用了一个call out函数__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__来执行任务。

__CFRunLoopDoBlocks

1
static Boolean __CFRunLoopDoBlocks(CFRunLoopRef rl, CFRunLoopModeRef rlm)

可以通过CFRunLoopPerformBlock将Block加入到一个指定的RunLoop中,Block不能自动唤醒RunLoop,只能在下一次其他任务来到时唤醒了RunLoop后,顺带处理一下Block。如果想让RunLoop马上处理该Block,可以手动唤醒RunLoop。

1
void CFRunLoopPerformBlock(CFRunLoopRef rl, CFTypeRef mode, void (^block)(void));

__CFRunLoopDoBlocks使用的call out函数是__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__

__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__

1
2
3
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(void *msg) {
_dispatch_main_queue_callback_4CF(msg);
}

开发者使用GCD的API将任务添加到main queue中,RunLoop收到了来自dispatch port端口的消息时,会调用该API来处理任务。该API不需要传入mode,也就是说main queue中的任务不是跟mode绑定的。

至此,总结完毕~

参考资料

【1】 苹果官方文档Run Loops

【2】 苹果官方文档CFRunLoop

【3】 深入理解RunLoop

【4】 解密 Runloop

【5】 RunLoop 详解

【6】 RunLoop 源码阅读

本文作者:意林
本文链接:http://shinancao.cn/2019/06/22/iOS-NSRunLoop/
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!