翻译自 Run, RunLoop, Run!html
尽管在开发者间不多讨论,但它是全部 app 中最重要的几个组件之一:Run Loops。Run Loops 就像是 app 跳动的心脏,它是让你的代码真正运行起来的东西。java
Run Loop 的基本原理实际上很简单。在 iOS 和 OSX 中,CFRunLoop
实现了供全部高层通讯和分发 API 使用的的核心机制。android
简单来讲,Run Loop 是一种通讯机制,用来完成异步或线程间通信。能够把它看做一个邮箱——等待消息并将它们发送给接收者们。git
Run Loop 要作两件事:github
在其余平台中(Win32),这种机制被叫作“消息泵(Message Pump)”。c#
Run Loop 是区分交互式应用和命令行工具的关键。命令行工具接受参数并启动,执行具体命令,最终退出。交互式应用则等待用户输入,作相应的反应,而后接着等待。事实上,这个基本机制在长时间运行的程序中也很常见。好比在服务器中的 while(1) {select();}
就是一个很好(虽然老)的 Run Loop 例子。服务器
Run loop 的工做就是等待某些事情发生。这些事情能够是由用户或者系统触发的外部事件(例如网络请求),或者是内部应用消息,例如线程间通知、异步代码执行、计时器等等。当收到一个事件(或者说消息)时,Run loop 找到一个相应的监听者并将消息传递给它。网络
实现一个基础的 Run loop 很容易。下面是一个简单的伪代码版本:闭包
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()
本身的 Run loop,而后使用 postMessage()
来和其余线程异步交换消息。个人同事 Cyril Mottier 告诉我 Android 版本的实现 并不比这个复杂多少。app
在 Apple 系统里面,这由 CFRunLoop
实现,一个稍微高级点的变形(CFRunLoop.c 有 3909 行,Looper.java 有 309 行)。除了早期初始化和你本身生成的线程,全部你写的代码都会在某个时刻被 CFRunLoop
调用。(据我所知,为 GCD 自动建立的线程不须要 CFRunLoop
,可是确定有一个消息系统来容许复用。)CFRunLoop
最重要的特性是 CFRunLoopModes
。CFRunLoop
与一个“Run Loop Sources”系统一同工做。注册在 Run Loop 上的 Sources 有一个或多种模式(modes),而 Run loop 自己就是在给定模式下运行的。当一个事件到达 source 时,只会交给有和 source 的模式匹配的 Run loop 来处理。
此外,CFRunLoop 是可重入的,不管是经过本身的代码或者框架内部代码。由于每一个线程都只有一个 CFRunLoop
,当一个组件想在一个特定模式下运行 Run Loop ,能够经过调用 CFRunLoopRunInMode()
实现。全部没有被注册为这个模式的 Run Loop source 都将被中止处理。一般该组件最终还会返回以前的模式。
CFRunLoop定义了一个伪模式:“公共模式”(kCFRunLoopCommonModes
),其实是一组“常规”的 Run Loop。主 Run Loop 开始是工做在 kCFRunLoopCommonModes
。另外一方面,UIKit 定义了一个特殊的 Run Loop 模式叫作 UITrackingRunLoopMode
。它在“当控制跟踪发生时”使用这个模式,例如触摸的时候。这很是重要,由于这保证了 TableView 的流畅滚动。当主线程的 Run Loop 在 UITrackingRunLoopMode
时,大部分后台事件,例如网络回调,都没有被分发。这样,没有了额外处理,滚动就不会卡顿(如今再卡顿的话,就是你的错了)。
若是你用堆栈跟踪调试过 iOS 和 OS X 代码,极可能你会在栈跟踪中注意到一个全大写的方法 CFRUNLOOP_IS_CALLING_OUT
。当 CFRunLoop
调用程序代码时,它就喜欢这么干。这里列出了 6 个定义在 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);
复制代码
观察者(Observers) 有一些特别。CFRunLoopObserver
API 容许你观察 CFRunLoop
的行为和它是否活跃(在处理事件或是正要去休眠等)。观察者在调试时很是有用,尤为是当你想了解 CFRunLoop
的特性的话。事实上,在一些特定的用途上它颇有用,例如:CoreAnimation
经过观察者调出(callout)来运行,这是有意义的,由于这样保证了全部 UI 代码都已经被执行,而且一次执行完全部的动画。
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(void (^block)(void));
复制代码
闭包(Blocks) 是 CFRunLoopPerformBlock()
API 的 另外一面,当你想在“下一个循环”运行代码时它很是有用。
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(void *msg);
复制代码
Main Dispatch Queue 标签是 CFRunLoop 对 GCD 的处理。显然,至少在主线程上,GCD 和 CFRunLoop
是协同工做的。即便 GCD 能够(而且会)创造没有 CFRunLoop
的线程,但当这里有一个 CFRunLoop
时,它会插入进去。
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(
CFRunLoopTimerCallBack func,
CFRunLoopTimerRef timer,
void *info);
复制代码
定时器(Timer) 相对容易从字面理解。在 iOS 和 OSX 中,高层 “定时器” 例如 NSTimer
或者 performSelector:afterDelay:
是经过 CFRunLoop
定时器实现的。从 iOS 7 和 Mavericks 开始,定时器的触发时间点有了一个容错区间的概念,这个特性固然也是 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 用 mach_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]
复制代码
就在 CGRunLoop.c 的这里。在上面几行,你能看到 Apple 工程师引用了哈姆雷特的独白:
/* In that sleep of death what nightmares may come ... */
复制代码
每当你的 app 运行时,CFRunLoop
的核心是 __CFRunLoopRun()
函数,经过公有 API CFRunLoopRun()
和 CFRunLoopRunInMode(mode, seconds, returnAfterSourceHandled)
来调用。
__CFRunLoopRun()
会由于四个缘由退出:
kCFRunLoopRunTimedOut
: 超时时,若是指定了间隔kCFRunLoopRunFinished
: 当它 “空”的时候,例如全部的资源都被删除了kCFRunLoopRunHandledSource
: 若是有 returnAfterSourceHandled
标志,在事件被发送以后kCFRunLoopRunStopped
: 经过 CFRunLoopStop()
手动中止在以上四个缘由之一出现以前,它会一直等待和分发事件。下面是一个包含咱们前文讨论的各类类型事件的处理过程的例子。
CFRunLoopPerformBlock()
API)。mach_port
,而后#ifdef
#elif
代码(b)代码中间有 goto
。主要的思路是,能够将 mach_msg()
配置为在多个队列和端口上等待。CFRunLoop
能够同时等待定时器、GCD 分发、手动唤醒或是去处理 Version 1 Sources。很简单吧?CoreFoundation
是由 C 实现的,看起来不怎么现代。我看到代码的第一反应是“这须要重构”。但另外一方面这些代码久经沙场,因此我不期待最近它会被用 Swift 重写。
有一种代码模式,我最近几年一直在使用,特别是在测试中。它就是“运行 run loop,直到这个条件变为真”,这是任何类型的异步单元测试的基础。随着时间的推移,我可能已经编写了不少这样的变体,直接使用 NSRunLoop
或 CFRunLoop
,进行轮询,使用超时等等。如今我能够编写一个像样的版本了,让咱们在下一篇文章中找到答案。