[译]奔跑吧!RunLoop!

翻译自 Run, RunLoop, Run!html

尽管在开发者间不多讨论,但它是全部 app 中最重要的几个组件之一:Run Loops。Run Loops 就像是 app 跳动的心脏,它是让你的代码真正运行起来的东西。java

Run Loop 的基本原理实际上很简单。在 iOS 和 OSX 中,CFRunLoop 实现了供全部高层通讯和分发 API 使用的的核心机制。android

Run Loop 究竟是什么?

简单来讲,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

iOS 和 OS X 呢

在 Apple 系统里面,这由 CFRunLoop 实现,一个稍微高级变形(CFRunLoop.c 有 3909 行,Looper.java 有 309 行)。除了早期初始化和你本身生成的线程,全部你写的代码都会在某个时刻被 CFRunLoop 调用。(据我所知,为 GCD 自动建立的线程不须要 CFRunLoop,可是确定有一个消息系统来容许复用。)CFRunLoop 最重要的特性是 CFRunLoopModesCFRunLoop 与一个“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 时,大部分后台事件,例如网络回调,都没有被分发。这样,没有了额外处理,滚动就不会卡顿(如今再卡顿的话,就是你的错了)。

揭秘 CFRunLoop

若是你用堆栈跟踪调试过 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 Sourcesmach_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 ... */
复制代码

悄悄看一眼 CFRunLoop.c

每当你的 app 运行时,CFRunLoop 的核心是 __CFRunLoopRun() 函数,经过公有 API CFRunLoopRun()CFRunLoopRunInMode(mode, seconds, returnAfterSourceHandled) 来调用。

__CFRunLoopRun() 会由于四个缘由退出:

  • kCFRunLoopRunTimedOut: 超时时,若是指定了间隔
  • kCFRunLoopRunFinished: 当它 “空”的时候,例如全部的资源都被删除了
  • kCFRunLoopRunHandledSource: 若是有 returnAfterSourceHandled 标志,在事件被发送以后
  • kCFRunLoopRunStopped: 经过 CFRunLoopStop() 手动中止

在以上四个缘由之一出现以前,它会一直等待和分发事件。下面是一个包含咱们前文讨论的各类类型事件的处理过程的例子。

  1. 调用闭包们(blocks, CFRunLoopPerformBlock() API)。
  2. 检查 Version 0 Sources, 并在必要时调用它们的 “perform” 函数。
  3. 轮训并内部调度队列和 mach_port,而后
  4. 若是没有东西须要处理,就去休眠。有什么事情的话内核会唤醒咱们。实际上这一块的代码要更加复杂,由于(a)为了 Win32 兼容性增长了不少 #ifdef #elif 代码(b)代码中间有 goto。主要的思路是,能够将 mach_msg() 配置为在多个队列和端口上等待。CFRunLoop 能够同时等待定时器、GCD 分发、手动唤醒或是去处理 Version 1 Sources。
  5. 唤醒,并尝试找出唤醒的缘由:
  6. 一个手动唤醒:继续运行这个循环,也许有一个闭包或者是 Version 0 Sources 在等待被处理。
  7. 一个或多个定时器被触发:调用定时器对应的方法。
  8. GCD 须要工做:经过特定的 “4CF” dispatch_queue API 来调用它。
  9. 内核发出了一个 Version 1 Source。找到并处理它。
  10. 再次调用闭包们。
  11. 检查退出条件。(结束、中断、超时、处理了 Source)(Finished, Stopped, TimedOut, HandledSource)
  12. 从头再来。

很简单吧?CoreFoundation 是由 C 实现的,看起来不怎么现代。我看到代码的第一反应是“这须要重构”。但另外一方面这些代码久经沙场,因此我不期待最近它会被用 Swift 重写。

有一种代码模式,我最近几年一直在使用,特别是在测试中。它就是“运行 run loop,直到这个条件变为真”,这是任何类型的异步单元测试的基础。随着时间的推移,我可能已经编写了不少这样的变体,直接使用 NSRunLoopCFRunLoop,进行轮询,使用超时等等。如今我能够编写一个像样的版本了,让咱们在下一篇文章中找到答案。

相关文章
相关标签/搜索