注:这篇文章翻译自 http://bou.io/RunRunLoopRun.html ,仅供学习参考,谢绝转载,已获得作者 Nicolas Bouilleaud 授权。
iOS 中有一个话题很少被开发者们提起,尽管它是所有 app 中最重要的组成元素之一,它就是 Runloop。Runloop 就像是 app 的心脏,你的代码因为有它才运行起来。
Runloop 的基本原则实际上很简单,在 iOS 和 OS X 上,CFRunloop 实现了被所有高层消息和调度 API 所使用的核心机制。
Runloop 到底是什么?
简单来说,runloop 是一个消息发送机制,用于异步的或者线程内的通信。它可以被看做一个信箱,等待消息并把消息发送出去。
Runloop 主要干两件事:
- 等待事件的发生(例如:消息到达),
- 发送消息给它的接收者。
在其他平台上,这个机制被称作“Message Pump”。
Runloop 把可交互的 app 和命令行程序区分开来。命令行程序带着参数启动,执行它们的命令,然后退出。可交互的 app 等待用户的输入,反应,然后继续等待。事实上,这个基本的机制在长时间运行的进程中也能找到。在服务器中的,一个 while(1){select();}
就可以看做 runloop。
Runloop 的工作是等待事情发生。这些事情可以是外部的事件,由用户或系统产生(例如网路请求)或者内部的 app 消息,例如线程内的通知,代码的异步执行,定时器...... 一旦一个事件(或者说消息)被接收,runloop 就会找到相应的监听者并把消息发送给它。
一个基本的 runloop 实际上很容易实现。下面是简单的伪代码:
func postMessage(runloop, message)
{
runloop.queue.pushBack(message)
runloop.signal()
}
func run(runloop)
{
do {
runloop.wait()
message = runloop.queue.popFront()
dispatch(message)
} while(true)
}
秉承着这个简单的机制,每个线程会 run()
它自己的 runloop,和其他线程的 runloop 通过 postMessage() 方法交换消息。我的同事 Cyril Mottier 向我指出 Android 的实现 不像那样复杂。
iOS 和 OS X 中又如何呢?
在苹果的系统中,这是 CFRunloop 的工作,是一个更高级的变体 。你写的所有代码都是在某个时刻被 CFRunloop 调用的,除了提前的初始化,或者你自己创建线程。(据我所知,GCD 队列自动创建的线程不需要 CFRunloop,但是也必然需要一个消息系统来方便重用。)
CFRunloop 最重要的特点是 CFRunLoopModes。CFRunloop 和一系统的“Run Loop Sources”一起工作。Sources 被注册到 runloop 的一个或多个 mode 中,runloop 被要求在一个指定的 mode 下运行。当一个事件到达 sources 时,当且仅当 source 的 mode 和 runloop 的当前 mode 相同时,事件才会被 runloop 处理。
另外,CFRunloop 可以从应用代码中重新进入,要么从你自己的代码中,要么从 framework 中。因为一个线程只有一个 CFRunloop,当一个元素想要在一个特定的 mode 下运行时,它需要调用 CFRunLoopRunInMode()
。所有没有注册进这个 mode 的 sources 会被停止服务。通常来说,那个元素最终会把控制权交给之前的 mode。
CFRunloop 定义了一个虚拟的 mode 称作 “common modes”(KCFRunloopCommonModes),它实际上是包含了 app 用到的一系列“常用”的 mode。比如,main runloop 在 kCFRunLoopCommonModes
下运行。
另一方面,UIKit 定义了一个特殊的 runloop mode,叫做 UITrackingRunLoopMode
。当对 controls 的追踪发生时,例如触摸事件,就会用到这个 mode。这很重要,因为这就是 tableview 流畅滚动的原因。当主线程的 runloop 在 UITrackingRunLoopMode
下运行时,大多数的后台事件,例如网络请求,就不会被发送了。就像这样,没有其他的工作在进行,滑动也没有延迟。(至少这时候应该是你的问题了。)
简单理解 CFRunloop
如果你曾经调试过 iOS 程序的堆栈信息,你应该已经发现,在堆栈信息的里面,所有的消息都以 CFRUNLOOP_IS_CALLING_OUT
开头。当 CFRunloop 调出程序代码时,它喜欢让它们显示出来。在 CFRunloop.c 里定义了六个这样的函数:
static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();
相信你猜到了,这些函数没有其他用途除了帮助调试堆栈信息。CFRunloop 保证了所有的程序代码都会调用其中某个函数。
让我们一个一个来看。
static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(
CFRunLoopObserverCallBack func,
CFRunLoopObserverRef observer,
CFRunLoopActivity activity,
void *info);
Observer 有点特殊。CFRunLoopObserber
API 让你能够观察 CFRunloop 的行为并且收到它活动的通知,例如当它在处理事件,当它进入休眠等等。这对调试来说起了很大的作用,你通常在你的 app 中不需要它,但是当你想实验 CFRunloop 的特性时它就很有帮助了。[2014-10-2 更细:事实上,它在其他的地方也有作用,例如 CoreAnimation 通过 Observser 的调出运行。它能够保证所有的 UI 代码已经开始运行,它会一次性的执行所有动画。]
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(
void (^block)(void));
Block 是 CFRunLoopPerformBlock()
API 的反面,当你想在下个循环里执行代码时很有用。
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(
void *msg);
Main Dispatch Queue 当然就是 CFRunloop 和 GCD 沟通的标志。很显然,至少在主线程中,GCD 和 CFRunloop 是手把手工作的。尽管 GCD 可以创建一个没有 CFRunloop 的线程,当有一个时,它会把自己塞进去。
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(
CFRunLoopTimerCallBack func,
CFRunLoopTimerRef timer,
void *info);
Timer 相对来说就很明了了。在 iOS 和 OS X 中,高层的 timer,例如 NSTimer 或者 performSelector:afterDelay:
是用 CFRunloop 的 timer 实现的。从 iOS 7 和 Mavericks 开始,timer 开始的时间有一个容忍度,这个特性也是 CFRunloop 提供的。
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(
void (*perform)(void *),
void *info);
CFRunloopSources
“Version 0” 和 “Version 1” 事实上是很不同的东西,尽管它们有相同的 API。Version 0 Sources 只是简单的应用内的消息传递机制,并且必须由程序代码手动的处理。在给一个 Version 0 Source(通过 CFRunLoopSourceSignal())发送信号后,CFRunloop 必须被唤醒(通过 CFRunLoopWakeUp())来处理这个 source。
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(
void *(*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info),
mach_msg_header_t *msg, CFIndex size, mach_msg_header_t **reply,
void (*perform)(void *),
void *info);
Version 1 Sources,另一方面来说,使用 math_port 处理内核事件。这实际上是 CFRunloop 的核心:大多数时候,当你的 app 什么也没干,它其实是在一个 mach_msg(…,MACH_RCV_MSG,…)
调用里阻塞着。如果你用 Activity Monitor 来观察一个任何一个 app,你很大程度上会看到下面的东西:
2718 CFRunLoopRunSpecific (in CoreFoundation) + 296 [0x7fff98bb7cb8]
2718 __CFRunLoopRun (in CoreFoundation) + 1371 [0x7fff98bb845b]
2718 __CFRunLoopServiceMachPort (in CoreFoundation) + 212 [0x7fff98bb8f94]
2718 mach_msg (in libsystem_kernel.dylib) + 55 [0x7fff99cf469f]
2718 mach_msg_trap (in libsystem_kernel.dylib) + 10 [0x7fff99cf552e]
代码在 CFRunloop 的这里,就在这代码的上面几行,苹果工程师注释了来自 Hamlet soliloquy 和这相关的引言:
/* In that sleep of death what nightmares may come ... */
CFRunloop.c 的一瞥
在你 app 运行的任何时候,CFRunloop 的核心就是 __CFRunLoopRun()
方法,被公共 API 方法 CFRunLoopRun()
和 CFRunLoopRunInMode(mode, seconds, returnAfterSourceHandled)
调用。
__CFRunLoopRun()
会因为四种原因退出:
-
kCFRunLoopRunTimedOut
:在超时后,如果规定了间隔的话, -
kCFRunLoopRunFinished
:当它变为空的后,例如,所有的 Source 都被移除了。 -
kCFRunLoopRunHandledSource
:当一个事件被处理后,并且携带着returnAfterSourceHandled
标志。 -
kCFRunLoopRunStopped
:被手动用CFRunLoopStop()
停止。
直到其中的一个原因发生,它会持续等待和发送事件。这里有一个单程,示例着处理上面所讨论的事件类型。
- 调用 “block”。(CFRunLoopPerformBlock() API)
- 检查 Version 0 Sources,如果必要的话调用它们的 “perform” 方法。
- Poll and internal dispatch queues and
mach_port
s, and (这句不知道怎么翻译,感觉有笔误) - 如果没有事件在等待就休眠。如果有事件就把它唤醒。其实在代码里面更复杂,因为在 Win32 的兼容代码里加了很多
#ifdef
#elif
,并且在代码中部有一个 goto。这里的主要想法是,mach_msg()
可以被配置来等待多个队列和 port。CFRunloop 通过这个来等同时待 timer,GCD 调度,手动唤醒,或者 Version 1 Sources。 - 被唤醒,并且尝试搞清楚原因:
- 手动唤醒。仅仅是继续运行这个 loop,可能有一个 block 或者 Version 0 Source 等待服务。
- 一个或多个 timer 发动了。调用它们的方法。
- GCD 需要工作。通过一个特殊的 “4CF” dispatch_queue API 来调用它。
- 内核给一个 Version 1 Source 发了一个信号。找到并且给他服务。
- 再次调用 “block”。
- 检查退出条件。(Finished, Stopped, TimedOut, HandledSource)
- 全部重新开始。
吁。是不是很简单。正如你所知道的,CoreFoundation 是用 C 实现的,看起来不怎么现代。在读这个的时候,我的第一反应是 “哇,这需要重构”。另一方面,这代码是经过测验的,所以我并不期望它会很快用 Swift 重写。
有一个代码模式我最近几年一直在用,特别是在测试的时候。它就是“运行 runloop 直到条件变为 true”,这是任何异步单元测试的基础。从以前到现在,我可能已经写了很多这样的代码,直接用 NSRunloop 或者 CFRunloop 来获取,使用超时时间等等。现在我应该可以写一个正规的版本了,下篇文章见。