建议搭配以上资料辅助阅读
首先,在一般情况下,代码的执行是线性的,执行完成之后就会退出返回:
int main(int argc, char *argv[]) {
NSLog(@"hello world");
return 0;
}
通常我们创建线程来处理自己的任务,也是这样的线性执行流程,当我们任务完成之后,便退出然后销毁线程。
但是对于一个 APP 来说,这种线性的执行流程,就不适用了。总不能让 APP 一打开,然后显示一下第一个页面,接着就马上退出了吧。得想一种办法,让 APP 的主线程能够一直驻留。在用户触发事件的时候,对其做出响应;在 APP 空闲的时候进入休眠,停止占用 CPU。这种模型通常被称为 Event Loop,事件循环。Run Loop 实现了这种事件处理机制。事件驱动型代码结构一般形式如下:
int main(int argc, char * argv[]) {
while(AppIsRunning) {
id whoWakesMe = SleepForWakingUp();
id event = GetEvent(whoWakesMe);
HandleEvent(event);
}
return 0;
}
Event Loop 结构中的重点有两部分:
SleepForWakingUp
函数,让线程在没有事件需要处理的时候陷入休眠,让出 CPU。当没有事件需要处理时,代码的实行会停在这个函数的调用处,线程在这里进入休眠状态;当事件到来时,线程被激活,whoWakesMe 获得返回值,代码从原来的休眠处重新跑起来,执行余下的操作。再举一个简单的例子,名为“程序猿的 main thread”:
// by @Sunnyxx
while(活着) {
有事干了 = 我睡觉了没事别叫我();
if (该搬砖了) {
搬砖();
} else if (该吃饭了) {
吃饭();
} else if (该陪妹子了) {
@throw(没有妹子);
}
}
在这里,我不负责任地用自己的话总结一下:run loop 是一种消息处理机制,它让线程能一直驻留而不退出,并且在闲时休眠,在事件到达时处理事件。
RunLoop 实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束(比如传入 quit 的消息),然后这个入口函数返回。(by ibireme)
因为有了 run loop 的存在,使得:
CFRunLoopRef 的源代码是开源,可以在这个链接下载到整个 CoreFoundation 的源码。为了方便跟踪和查看,你可以新建一个 Xcode 工程,把这堆源码拖进去看。
CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的;
NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。
GCD 跟 RunLoop 之间存在一些协作关系;mach kernel 让线程陷入休眠;block 为 run loop 提供业务代码;线程则更是 run loop 不可缺少的环节。
此外还有一些平时使用得多的类跟库,也是依赖于 run loop 的:
performSelector:AfterDelay:
之类的函数,实际上其内部会创建一个 timer 并添加到当前线程的 run loop 中。所以如果当前线程没有 run loop,则这个方法会失效。看看一个 sample 样例的调用堆栈:
start 是 dyld 干的,将程序调起来,然后是 main 函数,调用 UIApplicationMain
并返回。接着 Graphics Services 是处理硬件输入的,比如点击,所有的 UI 事件都是由它发出来的。接下来是 run loop ,最后是 UI 事件。
主线程中几乎所有函数都是从以下六个之一的函数调起的:
__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”,通俗来讲就是调出,往上层调用。
source 是 run loop 的数据源抽象类(id ),run loop 中存在两个 version 的 source:
CFRunLoopSourceSignal(source)
,将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
source 结构内部有一个联合体,version0 中的结构中,成员主要都是各种函数指针,这些都是 run loop 需要调用的方法。如果自己实现一个 source 的话需要一个一个填进去。重要的方法是最后一个 perform
方法,里面具体进行业务处理(此处从刚才Button的堆栈中也能体现出来)。
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),
kCFRunLoopBeforeTimers = (1UL << 1),
kCFRunLoopBeforeSources = (1UL << 2),
kCFRunLoopBeforeWaiting = (1UL << 5),
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopExit = (1UL << 7),
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
kCFRunLoopEntry 开始进入 run loop 了;使用 Observer 肯定会需要用到上面这个枚举,run loop 利用他们来告知 observer 目前自身的状态:使用 Observer 肯定会需要用到上面这个枚举,run loop 利用他们来告知 observer 目前自身的状态:
框架中的很多机制都是由 observer 来触发的,例如 CAAnimation(BeforeWaiting或者AfterWaiting时、汇集整个loop的Animation一起执行)。可以看下面关于界面更新的内容。
上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。
另外再看看 Observer 跟 Autorelease Pool (自动释放池)之间的关系:
CFRunLoop 与 Thread 之间是一一对应的,但不是说一个线程只能起一个 run loop,可以多个嵌套。RunLoop 不能直接创建,它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。 这两个函数内部的逻辑大概是下面这样:
/// 全局的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的。
run loop 必须在某种模式下来跑,系统预定义了几种模式。它们并不是一个 filter 的作用。mode 其实是一个 “树枝节点” ,Source、Observer以及Timer的几个节点实际上是在 mode 里面的。mode 对他们的存取方式如下:
run loop 在同一个时间段只能在一种特定的 mode 下 run,如果需要更换 mode 的话,需要先停止(应该是退出?)当前 loop,然后重新启动新 loop。mode 是 iOS App 滑动顺畅的关键。有以下几种 mode:
经典问题,UITrackingRunLoopMode 与 Timer:
[NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(timerTick:)
userInfo:nil
repeats:YES];
在主线程中调用上面的方法时, timer 默认被添加到 NSDefaultRunLoopMode 中,如果 scrollView 发生滑动,main run loop 会切换到 UITrackingRunLoopMode 下,于是 timer 便不会工作。如果要解决这个问题,可以将 timer 添加到 NSRunLoopCommonModes 中:
NSTimer *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 恢复回原来的模式(RunLoop始终是一个)。
CFRunLoop对外暴露的管理 Mode 接口只有下面2个:
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);
当你调用 CFRunLoopRunInMode() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。
Mode 暴露的管理 mode item 的接口有下面几个:
CFRunLoopAddSource(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 只能增加不能删除。
NSTimer 是对 CFRunLoopTimer 的上层封装(在上层调用的是内核MKTimer)。包括 performSelector:afterDelay:
里面使用的也是 RunLoopTimer。CFRunLoopTimerRef 是基于时间的触发器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
此外、RunLoop的Timer与GCD的Timer均为独立运行。
GCD 中 dispatch 到 main queue 的 block 是被分发到 main run loop 中执行的。这是由于 GCD 中的主线程跟 run loop 中的主线程是同一个。
假如使用 GCD 中的 dispatch_after,当时间到了之后,dispatch_after 才会将 block 放到 run loop 中去执行。
上面的 mach_msg
跟 mach_msg_trap
是指定某个 mach_port 然后发给内核的,trap 就是一个等待的消息,表示等待被唤醒,于是 run loop 便会暂停而被挂起。
挂起与唤醒过程:
mach_msg
监听唤醒端口。被唤醒前,系统内核将这个线程挂起,停留在 mach_msg_trap 状态
根据苹果的官方文档的描述,执行流程如下:
这里是 ibireme 的另一份伪代码:
/// 用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 addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
networkRequestThread
创建一个单例线程,线程跑起来之后先去跑 networkRequestThreadEntryPoint:
,然后在这个函数中创建这个线程的 run loop。新建完的 runLoop 如果没有事件处理的话就会直接退出了,所以让它随便监听一个 port,让它等待,一直活着。所以这个线程就可以一直驻留。这是一个创建常驻服务线程的好方法。
从调用堆栈可以看到,线程执行入口函数创建了 run loop 之后,停在 mach_msg_trap 状态,线程进入休眠。
网络图片下载完成之后去设置 cell 中的 imageView,会导致主线程“卡一下”。解决这个问题的最简单的方法,就是将设置图片的代码放到 NSDefaultRunLoopMode
中去运行:
UIImage *downloadedImage = ...;
[self.avatarImageView performSelector:@selector(setImage:)
withObject:downloadedImage
afterDelay:0
inModes:@[NSDefaultRunLoopMode]];
于是在滑动时不会设置 imageView,直到滑动停止 mode 切换为 defaultMode 才会执行设置 image 的代码。
//取当前 run loop
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
//取 run loop 所有运行的 mode
NSArray *allModes = CFBridgingRelease(CFRunLoopCopyAllModes(runLoop));
while (1) {
for (NSString *mode in allModes) {
//在每个 mode 中轮流运行至少 0.001 秒
CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
}
}
对于因为接收到 crash 的 signal 而挂掉的程序,可以在接收到 crash 的信号之后重新起一个 run loop 然后跑起来。但是这个并不能保证 app 能像原来一样能正常运行,只能是利用它来在奄奄一息的状态下弹出一些友好的错误信息。
原来写 test case 时最大的问题就是,它不支持异步。当时的一种解决方法是”每0.0001秒验证”:
- (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 前验证:
- (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;}