美文网首页
[翻译] Run, RunLoop, Run!

[翻译] Run, RunLoop, Run!

作者: Josscii | 来源:发表于2016-06-18 17:14 被阅读410次

    注:这篇文章翻译自 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));
    

    BlockCFRunLoopPerformBlock()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() 停止。

    直到其中的一个原因发生,它会持续等待和发送事件。这里有一个单程,示例着处理上面所讨论的事件类型。

    1. 调用 “block”。(CFRunLoopPerformBlock() API)
    2. 检查 Version 0 Sources,如果必要的话调用它们的 “perform” 方法。
    3. Poll and internal dispatch queues and mach_ports, and (这句不知道怎么翻译,感觉有笔误)
    4. 如果没有事件在等待就休眠。如果有事件就把它唤醒。其实在代码里面更复杂,因为在 Win32 的兼容代码里加了很多 #ifdef #elif,并且在代码中部有一个 goto。这里的主要想法是,mach_msg() 可以被配置来等待多个队列和 port。CFRunloop 通过这个来等同时待 timer,GCD 调度,手动唤醒,或者 Version 1 Sources。
    5. 被唤醒,并且尝试搞清楚原因:
      1. 手动唤醒。仅仅是继续运行这个 loop,可能有一个 block 或者 Version 0 Source 等待服务。
      2. 一个或多个 timer 发动了。调用它们的方法。
      3. GCD 需要工作。通过一个特殊的 “4CF” dispatch_queue API 来调用它。
      4. 内核给一个 Version 1 Source 发了一个信号。找到并且给他服务。
    6. 再次调用 “block”。
    7. 检查退出条件。(Finished, Stopped, TimedOut, HandledSource)
    8. 全部重新开始。

    吁。是不是很简单。正如你所知道的,CoreFoundation 是用 C 实现的,看起来不怎么现代。在读这个的时候,我的第一反应是 “哇,这需要重构”。另一方面,这代码是经过测验的,所以我并不期望它会很快用 Swift 重写。

    有一个代码模式我最近几年一直在用,特别是在测试的时候。它就是“运行 runloop 直到条件变为 true”,这是任何异步单元测试的基础。从以前到现在,我可能已经写了很多这样的代码,直接用 NSRunloop 或者 CFRunloop 来获取,使用超时时间等等。现在我应该可以写一个正规的版本了,下篇文章见。

    相关文章

      网友评论

          本文标题:[翻译] Run, RunLoop, Run!

          本文链接:https://www.haomeiwen.com/subject/qvfbdttx.html