美文网首页iOS面试iOS学习
Cocoa RunLoop 系列之基础知识

Cocoa RunLoop 系列之基础知识

作者: IcebergHorseman | 来源:发表于2016-12-08 15:21 被阅读51次

    博客地址

    这篇博客主要结合Apple开发者文档和个人的理解,写的一篇关于Cocoa RunLoop基本知识点的文章。在文档的基础上,概况和梳理了RunLoop相关的知识点。

    一、Event Loop & Cocoa RunLoop

    宏观上:Event Loop

    1. RunLoop是一个用于循环监听和处理事件或者消息的模型,接收请求,然后派发给相关的处理模块,wikipedia上有更为全面的介绍:Event_loop
    2. Cocoa RunLoop属于Event Loop模型在Mac平台的具体实现
    3. 其他平台的类似实现:X Window程序,Windows程序 ,Glib库等

    微观上: Cocoa RunLoop

    1. Cocoa RunLoop本质上就是一个对象,提供一个入口函数启动事件循环,在满足特点条件后才会退出。
    2. Cocoa RunLoop与普通while/for循环不同的是它能监听处理事件和消息,能智能休眠和被唤醒,这些功能的其实现依赖于Mac Port。

    二、 Cocoa RunLoop的内部结构

    但凡说到Cocoa RunLoop内部结构,都离不开下面这张图,来源于Apple开发者文档

    图1-1 RunLoop结构图

    结合上图,可将RunLoop架构划分为四个部分:

    1. 事件源
    2. 运行模式
    3. 循环机制
    4. 执行反馈

    1. 事件源

    Cocoa RunLoop接受的事件源分为两种类型:Input Sources 和 Timer Sources

    1.1. Input Sources

    Input Sources通过异步派发的方式将事件转送到目标线程,事件类别分为两大块:

    • Port-Based Sources :

      基于Mach端口的事件源,Cocoa和Core Foundation这两个框架已经提供了内部支持,只需要调用端口相关的对象或者函数就能提供端口进行通信。比如:将NSPort对象部署到RunLoop中,实现两个线程的循环通信。

    • Custom Input Sources :

      • 用户自定义的输入源:使用Core Foundation框架中CFRunLoopSourceRef对象的相关函数实现。具体实现可以查看另外一篇博客:Cocoa RunLoop 系列之Configure Custom InputSource

      • Cocoa Perform Selector Sources:Cocoa框架内部实现的自定义输入源,可以跨线程调用,实现线程见通信,有点类似于Port-Based事件源,不同的是这种事件源只在RunLoop上部署一次,执行结束后便会自动移除。如果目标线程中没有启动RunLoop也就意味着无法部署这类事件源,因此不会得到预期的结果。

        使用Cocoa自定义事件源的函数接口,如下:

    
       //部署在主线程
       //参数列表:Selector:事件源处理函数,Selector参数,是否阻塞当前线程,指定RunLoop模式
       performSelectorOnMainThread:withObject:waitUntilDone:
       performSelectorOnMainThread:withObject:waitUntilDone:modes:
       
       //部署在指定线程
       //参数列表:Selector:事件源处理函数,指定线程,Selector参数,是否阻塞当前线程,指定RunLoop模式
       permSelector:onThread:withObject:waitUntilDone:
       performSelector:onThread:withObject:waitUntilDone:modes:
       
       //部署在当前线程
       //参数列表:Selector:事件源处理函数,Selector参数,延时执行时间,指定RunLoop模式
       performSelector:withObject:afterDelay:
       performSelector:withObject:afterDelay:inModes:
        
       //撤销某个对象通过函数performSelector:withObject:afterDelay:部署在当前线程的全部或者指定事件源
       cancelPreviousPerformRequestsWithTarget:
       cancelPreviousPerformRequestsWithTarget:selector:object:
    
    

    综上,Input Sources包括基于Mach端口的事件源和自定义的事件源,二者的唯一区别在于被触发的方式:前者是由内核自动触发,后者则需要在其他线程中手动触发。

    1.2. Timer Sources

    不同于Input Sources的异步派发,Timer Source是通过同步派发的方式,在预设时间到达时将事件转送到目标线程。这种事件源可用于线程的自我提醒功能,实现周期性的任务。

    • 如果RunLoop当前运行模式没有添加Time Sources,则在RunLoop中部署的定时器不会被执行。
    • 设定的间隔时间与真实的触发时间之间没有必然联系,定时器会根据设定的间隔时间周期性的派发消息到RunLoop,但是真实的触发时间由RunLoop决定,假设RunLoop当前正在处理其一个长时间的任务,则触发时间会被延迟,如果在最终触发之前Timer已经派发了N个消息,RunLoop也只会当做一次派发对待,触发一次对应的处理函数。

    2. 运行模式

    运行模式类似于一个过滤器,用于屏蔽那些不关心的事件源,让RunLoop专注于监听和处理指定的事件源和RunLoop Observer。

    CFRunLoopMode 和 CFRunLoop 的数据结构大致如下:

    
        struct __CFRunLoop {
            CFMutableSetRef _commonModes;     // Set
            CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
            CFRunLoopModeRef _currentMode;    // Current Runloop Mode
            CFMutableSetRef _modes;           // Set
            ...
        };
        
        struct __CFRunLoopMode {
            CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
            CFMutableSetRef _sources0;    // Set
            CFMutableSetRef _sources1;    // Set
            CFMutableArrayRef _observers; // Array
            CFMutableArrayRef _timers;    // Array
            ...
        };
    
    

    结合以上源码,总结以下几点:

    • 每种模式通过name属性作为标识。
    • 一种运行模式(Run Loop Mode)就是一个集合,包含需要监听的事件源Input Sources和Timer Soueces以及需要触发的RunLoop observers。
    • Cocoa RunLoop包含若干个Mode,调用RunLoop是指定的Mode称之为CurrentMode。RunLoop可以在不同的Mode下切换,切换时退出CurrentMode,并保存相关上下文,再进入新的Mode。
    • 在启动Cocoa RunLoop是必须指定一种的运行模式,且如果指定的运行模式没有包含事件源或者observers,RunLoop会立刻退出。
    • CFRunLoop结构中的commonModes是Mode集合,将某个Mode的name添加到commonModes集合中,表示这个Mode具有“common”属性。
    • CFRunLoop结构中的commonModeItems则是共用源的集合,包括事件源和执行反馈。这些共用源会被自动添加到具有“common”属性的Mode中。

    ** Note ** : 不同的运行模式区别在于事件源的不同,比如来源于不同端口的事件和端口事件与Timer事件。不能用于区分不同的事件类型,比如鼠标消息事件和键盘消息事件,因为这两种事件都属于基于端口的事件源。

    以下是苹果预定义好的一些运行模式:

    • NSDefaultRunLoopMode //默认的运行模式,适用于大部分情况
    • NSConnectionReplyMode //Cocoa库用于监听NSConnection对象响应,开发者很少使用
    • NSModalPanelRunLoopMode //模态窗口相关事件源
    • NSEventTrackingRunLoopMode //鼠标拖拽或者屏幕滚动时的事件源
    • NSRunLoopCommonModes //用于操作RunLoop结构中commonModes和commonModeItems两个属性

    3. 循环机制

    循环机制涉及两方面:

    3.1. RunLoop与线程之间的关系

    Apple文档中提到:开发者不需要手动创建RunLoop对象,每个线程包括主线程都关联了一个RunLoop对象。除了主线程的RunLoop在程序启动时被开启,其他线程的RunLoop都需要手动开启。

    待解决的疑问:

    • 线程中的RunLoop是一直存在还是需要时再创建?
    • 线程与RunLoop的是如何建立联系的?
    • 线程与RunLoop对象是否是一一对应的关系?
    3.2. RunLoop事件处理流程

    弄清楚RunLoop内部处理逻辑是理解RunLoop的关键,将单独写一篇博客进行分析。

    待解决的疑问:

    • RunLoop如何处理不同事件源?
    • RunLoop不同模式切换是如何实现的?

    以上两方面,将在下一篇博客Cocoa RunLoop 系列之源码解析中结合源代码来找到答案。

    4. 执行反馈

    RunLoop Observers机制属于RunLoop一个反馈机制,将RunLoop一次循环划分成若干个节点,当执行到对应的节点调用相应的回调函数,将RunLoop当前的执行状态反馈给用户。

    • 用户可以通过Core Foundation框架中的CFRunLoopObserverRef注册 observers。

    • 监听节点:

      • The entrance to the run loop. //RunLoop启动
      • When the run loop is about to process a timer. //即将处理Timer事件源
      • When the run loop is about to process an input source. //即将处理Input事件源
      • When the run loop is about to go to sleep. //即将进入休眠
      • When the run loop has woken up, but before it has processed the event that woke it up. //重新被唤醒,且在处理唤醒事件之前
      • The exit from the run loop. //退出RunLoop
    • 监听类别分为两种:一次性和重复监听。

    三、何时使用RunLoop

    由于主线程的RunLoop在程序启动时被自动创建并执行,因此只有在其他线程中才需要手动启动RunLoop。很多情况下,对于RunLoop的使用多数情况是在主线程中,包括进行RunLoop模式切换,设置RunLoop Observer等。

    在非主线程中,以下几种情况适用于RunLoop:

    • 使用基于端口或者自定义的事件源与其他线程进行通信。
    • 需要在当前线程中使用Timer,必须部署才RunLoop中才有效。
    • 在目标线程中调用performSelector… 函数,因为本质上使用了Cocoa自定义的事件源,依赖于RunLoop才能被触发。
    • 线程需要进行周期性的任务,需要长时间存在,而非执行一次。

    四、总结

    一直以来,RunLoop对我来说都属于一个比较模糊的概念,在实际编程中也有用到RunLoop的一些功能,确实感觉到很强大,但是仅仅停留在应用层面,并不是很理解具体含义。因此,为了更好的使用RunLoop,有必要研究和梳理RunLoop相关的知识点。

    相关文章

      网友评论

        本文标题:Cocoa RunLoop 系列之基础知识

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