目录在右边→_→,点右下角三道杠粗现。
RunLoop 是什么鬼
首先,在一般情况下,代码的执行是线性的,执行完成之后就会退出返回:1
2
3
4int main(int argc, char *argv[]) {
NSLog(@"hello world");
return 0;
}
通常我们创建线程来处理自己的任务,也是这样的线性执行流程,当我们任务完成之后,便退出然后销毁线程。
但是对于一个 APP 来说,这种线性的执行流程,就不适用了。总不能让 APP 一打开,然后显示一下第一个页面,接着就马上退出了吧。得想一种办法,让 APP 的主线程能够一直驻留。在用户触发事件的时候,对其做出响应;在 APP 空闲的时候进入休眠,停止占用 CPU。这种模型通常被称为 Event Loop,事件循环。Run Loop 实现了这种事件处理机制。事件驱动型代码结构一般形式如下:1
2
3
4
5
6
7
8
9int main(int argc, char * argv[]) {
while(AppIsRunning) {
id whoWakesMe = SleepForWakingUp();
id event = GetEvent(whoWakesMe);
HandleEvent(event);
}
return 0;
}
Event Loop 结构中的重点有两部分:
- 外部的 while 循环结构,它保证了线程在处理完事件之后不会退出;
SleepForWakingUp
函数,让线程在没有事件需要处理的时候陷入休眠,让出 CPU。当没有事件需要处理时,代码的实行会停在这个函数的调用处,线程在这里进入休眠状态;当事件到来时,线程被激活,whoWakesMe 获得返回值,代码从原来的休眠处重新跑起来,执行余下的操作。
再举一个简单的例子,名为“程序猿的 main thread”:1
2
3
4
5
6
7
8
9
10
11
12// by @Sunnyxx
while(活着) {
有事干了 = 我睡觉了没事别叫我();
if (该搬砖了) {
搬砖();
} else if (该吃饭了) {
吃饭();
} else if (该陪妹子了) {
@throw(没有妹子);
}
}
在这里,我不负责任地用自己的话总结一下:run loop 是一种消息处理机制,它让线程能一直驻留而不退出,并且在闲时休眠,在事件到达时处理事件。
RunLoop 实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束(比如传入 quit 的消息),然后这个入口函数返回。(by ibireme)
因为有了 run loop 的存在,使得:
- 程序能一直运行并接受用户输入
- 决定程序在何时该处理哪些事件
- 调用解耦:例如主调方产生事件之后放入消息队列,让被调方自己取来处理,而不必等待被调方返回
- 节省 CPU 时间:没事件处理时休眠
CFRunLoopRef 的源代码是开源,可以在这个链接下载到整个 CoreFoundation 的源码。为了方便跟踪和查看,你可以新建一个 Xcode 工程,把这堆源码拖进去看。
Run Loops in Cocoa
CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的;
NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。
GCD 跟 RunLoop 之间存在一些协作关系;mach kernel 让线程陷入休眠;block 为 run loop 提供业务代码;线程则更是 run loop 不可缺少的环节。
Example1
此外还有一些平时使用得多的类跟库,也是依赖于 run loop 的:
- NSTimer: 每个 timer 都必须得添加到 run loop 中才能跑起来;
- UIEvent: 时间的产生,分发,到代码执行都是通过 run loop 在跑的;
- Autorelease: 本次 run loop 结束时会将本次 loop 内产生的所有 Autorelease 对象释放,事件大约在本次 run loop 休眠之后,下次 run loop 休眠之前的某个时间点。下面会提到;
- Selector: 要想在线程中执行 selector,线程中必须有一个正在运行的 run loop;
- NSDelayedPerforming:
performSelector:AfterDelay:
之类的函数,实际上其内部会创建一个 timer 并添加到当前线程的 run loop 中。所以如果当前线程没有 run loop,则这个方法会失效。 - NSThreadPerformAddition: 跟 4. 同,另外实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。(by Ibireme)
- 界面更新相关: 改变了 UI 的 frame 或者是 UIView/CALayer 的层次等等更新了界面之后,会在 run loop 的 observer 中执行实际的绘制和调整,下面会提到;
- dispatch_get_main_queue: block 会在主线程的 run loop 中得到执行,下详;
- NSURLConnection: delegate 和网络回来的数据都是在 run loop 中跑的,下详;
Example2
看看一个 sample 样例的调用堆栈:
start 是 dyld 干的,将程序调起来,然后是 main 函数,调用 UIApplicationMain
并返回。接着 Graphics Services 是处理硬件输入的,比如点击,所有的 UI 事件都是由它发出来的。接下来是 run loop ,最后是 UI 事件。
主线程中几乎所有函数都是从以下六个之一的函数调起的:1
2
3
4
5
6__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
长得比较丑陋,当然,这么长串的名字是为了在调用栈里面自解释。上面的函数都是 “Call Out”,通俗来讲就是调出,往上层调用。
RunLoop 机制
RunLoop 与 线程之间的关系:
CFRunLoop 与 Thread 之间是一一对应的,但不是说一个线程只能起一个 run loop,可以多个嵌套。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
38/// 全局的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 _CFRunLoopGet(pthread_main_thread_np());
}
CFRunLoopRef CFRunLoopGetCurrent() {
return _CFRunLoopGet(pthread_self());
}
线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。
RunLoop Mode
run loop 必须在某种模式下来跑,系统预定义了几种模式。它们并不是一个 filter 的作用。mode 其实是一个 “树枝节点” ,底下紫色的几个节点实际上是在 mode 里面的。mode 对他们的存取方式如下:
run loop 在同一个时间段只能在一种特定的 mode 下 run,如果需要更换 mode 的话,需要先停止(退出)的话当前 loop,然后重新启动新 loop。mode 是 iOS App 滑动顺畅的关键。有以下几种 mode:
- NSDefaultRunLoopMode:默认的状态,也是空闲的状态——对 APP 没有操作时,main run loop 就会处于这个 mode;
- UITrackingRunLoopMode:滑动 ScrollView 时会切换到这个 mode;
- UIInitializationRunLoopMode:私有,在 APP 启动时会处于这个 mode,启动后 切到 default;
- NSRunLoopCommonModes:默认情况下包含 1 与 2 两种 mode;
经典问题,UITrackingRunLoopMode 与 Timer:1
2
3
4
5[NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(timerTick:)
userInfo:nil
repeats:YES];
在主线程中调用上面的方法时, timer 被添加到 NSDefaultRunLoopMode 中,如果 scrollView 发生滑动,main run loop 会切换到 UITrackingRunLoopMode 下,于是 timer 便不会工作。如果要解决这个问题,可以将 timer 添加到 NSRunLoopCommonModes 中:1
2
3
4
5
6NSTimer *timer = [NSTimer timerWithTimeInterval:1.0
target:self
selector:@selector(timerTick:)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
另在再来看看 RunLoopMode 切换时的调用堆栈:
开始滑动时,run loop 停止,然后利用 pushRunLoopMode
将 run loop 切换到 tracking mode 下;滑动停止,利用 popRunLoopMode
将 run loop 恢复回原来的模式。
CFRunLoop对外暴露的管理 Mode 接口只有下面2个:1
2CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);
当你调用 CFRunLoopRunInMode() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。
Mode 暴露的管理 mode item 的接口有下面几个:1
2
3
4
5
6CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
你只能通过 mode name 来操作内部的 mode,当你传入一个新的 mode name 但 RunLoop 内部没有对应 mode 时,RunLoop会自动帮你创建对应的 CFRunLoopModeRef。对于一个 RunLoop 来说,其内部的 mode 只能增加不能删除。
RunLoop Timer
NSTimer 是对 CFRunLoopTimer 的上层封装。包括 performSelector:afterDelay:
里面使用的也是 RunLoopTimer。CFRunLoopTimerRef 是基于时间的触发器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
CFRunLoopSource
source 是 run loop 的数据源抽象类(id
- source0,处理 APP 内部事件,APP 自己负责管理(触发),例如 touch 事件,UIEvent,CFSocket。source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用
CFRunLoopSourceSignal(source)
,将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。 - source1,由 run loop 和内核管理,Mach port 驱动,例如 CFMachPort,CFMessagePort。关于 port:给某个进程发消息可以发到某个 port 上,如果进程监听这个 port,就可以收到这个消息。注意,是进程。一个 app 就是一个进程。
source 结构内部有一个联合体,version0 中的结构中,成员主要都是各种函数指针,这些都是 run loop 需要调用的方法。如果自己实现一个 source 的话需要一个一个填进去。重要的方法是最后一个 perform
方法,里面进行业务处理。
CFRunLoopObserver
1 | typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { |
使用 Observer 肯定会需要用到上面这个枚举,run loop 利用他们来告知 observer 目前自身的状态:
- kCFRunLoopEntry 开始进入 run loop 了;
- kCFRunLoopBeforeTimers 要执行 timer 了;
- kCFRunLoopBeforeSources 要执行 source 了;
- kCFRunLoopBeforeWaiting 将要睡眠了;
- kCFRunLoopAfterWaiting run loop 被唤醒了;
- kCFRunLoopExit run loop 退出了;
框架中的很多机制都是由 observer 来触发的,例如 CAAnimation。可以看下面关于界面更新的内容。
上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。
另外再看看 Observer 跟 Autorelease Pool 之间的关系:
RunLoop 与 GCD 的关系
GCD 中 dispatch 到 main queue 的 block 是被分发到 main run loop 中执行的。这是由于 GCD 中的主线程跟 run loop 中的主线程是同一个。
假如使用 GCD 中的 dispatch_after,当时间到了之后,dispatch_after 才会将 block 放到 run loop 中去执行。
RunLoop 的挂起和唤醒
上面的 mach_msg
跟 mach_msg_trap
是指定某个 mach_port 然后发给内核的,trap 就是一个等待的消息,表示等待被唤醒,于是 run loop 便会暂停而被挂起。
挂起与唤醒过程:
- 在 run loop 进入等待前,先要指定一个用于唤醒的 mach_port
- 然后调用
mach_msg
监听唤醒端口。被唤醒前,系统内核将这个线程挂起,停留在 mach_msg_trap 状态 - 由另一个线程(或者另一个进程中的某个线程)向内核发送这个端口的 msg,trap 状态被唤醒,run loop 继续还是处理任务
RunLoop 迭代执行顺序
- 第一行设置过期时间,这是通过 GCD 的 timer 来监测的;
- 通知 observer 相关 run loop 状态;
- 执行 block,执行添加到 run loop 中的 source0;
- 向 GCD 查询是否有需要分派到主线程的任务;
- 进入休眠,通知 observer 即将进入休眠了;
- SleepAndWaitForWakingUpPorts() 让线程进入休眠,等待消息来唤醒,即上面提到的 mach_msg_trap 状态;
- 当消息来了,于是 wakeUpPort 得到返回值,根据返回值来执行业务处理;
根据苹果的官方文档的描述,执行流程如下:
这里是 ibireme 的另一份伪代码: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/// 用DefaultMode启动
void CFRunLoopRun(void) {
CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}
/// 用指定的Mode启动,允许设置RunLoop超时时间
int CFRunLoopRunInMode(CFStringRef 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. 通知 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 实践
AFNetWorking
注意这行代码:1
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
networkRequestThread
创建一个单例线程,线程跑起来之后先去跑 networkRequestThreadEntryPoint:
,然后在这个函数中创建这个线程的 run loop。新建完的 runLoop 如果没有事件处理的话就会直接退出了,所以让它随便监听一个 port,让它等待,一直活着。所以这个线程就可以一直驻留。这是一个创建常驻服务线程的好方法。
从调用堆栈可以看到,线程执行入口函数创建了 run loop 之后,停在 mach_msg_trap 状态,线程进入休眠。
TableView 延时加载图片
网络图片下载完成之后去设置 cell 中的 imageView,会导致主线程“卡一下”。解决这个问题的最简单的方法,就是将设置图片的代码放到 NSDefaultRunLoopMode
中去运行:1
2
3
4
5UIImage *downloadedImage = ...;
[self.avatarImageView performSelector:@selector(setImage:)
withObject:downloadedImage
afterDelay:0
inModes:@[NSDefaultRunLoopMode]];
于是在滑动时不会设置 imageView,直到滑动停止 mode 切换为 defaultMode 才会执行设置 image 的代码。
让 Crash 掉的 APP 回光返照
1 | //取当前 run loop |
对于因为接收到 crash 的 signal 而挂掉的程序,可以在接收到 crash 的信号之后重新起一个 run loop 然后跑起来。但是这个并不能保证 app 能像原来一样能正常运行,只能是利用它来在奄奄一息的状态下弹出一些友好的错误信息。
Async Test Case
原来写 test case 时最大的问题就是,它不支持异步。当时的一种解决方法是”每0.0001秒验证”:1
2
3
4
5
6
7
8- (void)runUntilBlock:(BOOL(^)())block timeout:(NSTimeInterval)timeout
{
NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:timeout];
do {
CFTimeInterval quantum = 0.0001;
CFRunLoopRunInMode(kCFRunLoopDefaultMode, quantum, false);
} while([timeoutDate timeIntervalSinceNow] > 0.0 && !block());
}
这是原来的方案,后来更新了,换成了 run loop sleep 前验证:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22- (BOOL)runUntilBlock:(BOOL(^)())block timeout:(NSTimeInterval)timeout
{
__block Boolean fulfilled = NO;
void (^beforeWaiting) (CFRunLoopObserverRef observer, CFRunLoopActivity activity) =
^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
fulfilled = block();
if (fulfilled) {
CFRunLoopStop(CFRunLoopGetCurrent());
}
};
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopBeforeWaiting, true, 0, beforeWaiting);
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// Run!
CFRunLoopRunInMode(kCFRunLoopDefaultMode, timeout, false);
CFRunLoopRemoveObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
CFRelease(observer);
return fulfilled;
}
参考(抄袭)资料
- 深入理解RunLoop,by @Ibireme
- 孙源的线下分享视频低清在线,高清无码视频,Key Note 文件,by @Sunnyxx
- RunLoop 的苹果官方文档
以上