美文网首页
看苹果官方文档怎么说RunLoop

看苹果官方文档怎么说RunLoop

作者: 拧发条鸟xds | 来源:发表于2018-08-23 20:55 被阅读0次

    这几天研究了一下iOS的Runloop,看了不少的文章,收获不少,但是疑问也挺多。所以我就试着去翻译了并分析总结了一下苹果的Runloop文档,注意:并不是CFRunloop的源码,而是这篇总体概述了Runloop的文档

    这里先给出一些有关RunLoop的官方文档及一些好的文章的链接:

    官方文档:

    Core Foundation框架源码

    CFRoopLoop源码

    苹果对于Runloop的概括说明文档

    CFRunLoop的官方参考文档

    NSRunLoop的官方参考文档

    入门可以看:

    《iOS RunLoop入门小结》

    《RunLoop入门 看我就够了》

    大神文章:

    《深入理解RunLoop》

    《iOS刨根问底-深入理解RunLoop》

    《iOS程序启动与运转——RunLoop个人小结》

    RunLoop怎么用:

    《RunLoop已入门?不来应用一下?》

    RunLoop问题集:

    《RunLoop问题集》

    《iOS基础面试题之RunLoop篇》


    提前说明,这篇文章你可以对照着英文文档来看,目录结构是一样的。但并不是逐句翻译的,会有省略,并加上了我自己的总结分析。

    本文目录

    • Run Loops(RunLoop)
    • Anatomy of a Run Loop(RunLoop的剖析)
    • Run Loop Modes(RunLoop的Mode)
    • Input Sources
        1. Port-Based Sources(基于端口的输入源)
        1. Custom Input Sources(自定义输入源)
        1. Cocoa Perform Selector Sources(PerformSelector源)
    • Timer Sources(定时器源)
    • Run Loop Observers(RunLoop的观察者Observer)
    • The Run Loop Sequence of Events(RunLoop的内部运行逻辑)
    • 关于CFRunLoop(这个为个人补充)
    • 关于CFRunLoopMode(这个为个人补充)
    • When Would You Use a Run Loop?(什么时候使用RunLoop?)
    • Using Run Loop Objects (如何使用RunLoop对象)
        1. Getting a Run Loop Object(获取RunLoop对象)
        1. Configuring the Run Loop(配置RunLoop)
        1. Starting the Run Loop(启动RunLoop)
        1. Exiting the Run Loop(退出RunLoop)
        1. Thread Safety and Run Loop Objects(线程安全和RunLoop对象)
    • Configuring Run Loop Sources(配置RunLoop的Source)
        1. Defining a Custom Input Source(定义自定义输入源,即Source0)
        1. Configuring Timer Sources(配置定时器源,即Timer)
        1. Configuring a Port-Based Input Source(配置基于端口的输入源,即Source1)

    Run Loops(RunLoop)

    RunLoop是与线程相关的基础架构中的一部分。RunLoop是一个事件处理环,它可以用来安排工作并协调传入的事件。RunLoop的目的是在有任务的时候保持线程的运行,在没有任务的时候使线程休眠。

    RunLoop的使用管理不是完全自动的。你必须在合适的时机使用线程代码启动RunLoop,并对接收的事件进行反应处理。Cocoa(NSRunLoop)和Core Foundation(CFRunLoop)都提供了RunLoop的对象。

    每一个线程都对应着一个RunLoop。在应用程序启动的时候,主线程的RunLoop会自动启动,而辅助线程(即子线程)是需要你手动去获取的

    这里有CFRunLoopNSRunLoop的参考文档。

    Anatomy of a Run Loop(RunLoop的剖析)

    RunLoop与它的名字的意思相像,它是一个环,线程进入这个环并且可以对收到的事件进行运行和处理。

    image

    如上图所示:RunLoop从两种不同类型的源接收事件,一个是输入源(Input Source),另一个是定时器源(Timer Source)。Input Source 提供异步事件,通常是来自另一个线程或不同应用程序的消息Timer Source提供同步事件,是发生在预定时间的或重复间隔里的

    除了处理输入的源,RunLoop还会生成有关RunLoop的通知,已经注册了的Observer可以接收这些通知并使用它们在线程上执行其它的处理。你可以通过Core Foundation在你的线程上使用RunLoop Observer。

    分析说明:

    • RunLoop有两种源:Input sources和Timer sources,Input sources里面又分了几种。RunLoop还包含了Observer。
    • 看图,Input Source里面有三种:Port、Custom、performSelector,其实只有两种:基于端口的输入源(Port)和自定义的输入源(Custom),因为performSelector是苹果自定义的输入源(它比较特殊)。
    • 关于CFRunLoop的里面的Source又分为了Source1和Source0,我们下面会说。
    下面如无特别说明,Source就是指Input sources,Timer就是指Timer sources。

    Run Loop Modes(RunLoop的Mode)

    一个RunLoop Mode是多个输入源(Input Source)、多个定时器(Timer)、多个观察者(Observer)的集合。每次你运行一个RunLoop,你都得显式或隐式地指定要运行的Mode。在RunLoop的运行过程中,仅监测该Mode下的源(sources)并允许其传递事件,并且只有该Mode下的Observer才有作用。在其它Mode下的源(sources)会挂起任何新的事件,直到RunLoop以在该Mode下运行。

    分析说明:

    • 每个线程只能有一个对应的RunLoop,RunLoop必须手动去开启才能存在,但是主线程对应的RunLoop是在应用启动的时候自动就开启了,所以只需要你主动去开启子线程的RunLoop不用管主线程的RunLoop。关于RunLoop的创建下面会说。现在先了解这一点。

    分析说明:
    一般我们常用的Mode有三种:

    1. kCFRunLoopDefaultMode(CFRunLoop)/NSDefaultRunLoopMode(NSRunLoop)

    默认模式,在RunLoop没有指定Mode的时候,默认就跑在DefaultMode下。一般情况下App都是运行在这个mode下的

    1. CFStringRef)UITrackingRunLoopMode(CFRunLoop)/UITrackingRunLoopMode(NSRunLoop)

    一般作用于ScrollView滚动的时候的模式,保证滑动的时候不受其他事件影响。

    1. kCFRunLoopCommonModes(CFRunLoop)/NSRunLoopCommonModes(NSRunLoop)
      这个并不是某种具体的Mode,而是一种模式组合,在主线程中默认包含了NSDefaultRunLoopMode和 UITrackingRunLoopMode。子线程中只包含NSDefaultRunLoopMode。

    注意:
    在选择RunLoop的runMode时不可以填这种模式否则会导致RunLoop运行不成功
    在添加事件源的时候填写这个模式就相当于向组合中所有包含的Mode中注册了这个事件源
    ③你也可以通过调用CFRunLoopAddCommonMode()方法将自定义Mode放到kCFRunLoopCommonModes组合。

    分析说明:

    • 注意,一个RunLoop里会有多个Mode(这点后面会说明);
    • 一个Mode下有多个Source、Timer、Observer;
    • RunLoop的运行必须指定一个Mode,不管是显式或隐式的指定;
    • RunLoop在一个Mode下运行,该Mode里的Source、Timer、Observer才会有效,其它Mode里的Source、Timer、Observer就不能有效果了。所以说Mode其实是为了把不同的Source、Timer、Observer分开来;
    • 其它没有运行的Mode会挂起的新来的事件,只有当RunLoop运行到该Mode下时,该Mode的新事件才会被处理。

    你可以通过Mode的名字来识别和使用这些Mode。Cocoa 和 Core Foundation都定义了一个默认的Mode和其它一些常用的Mode。你也可以使用任何名称自定义一个Mode,但必须确保这个自定义的Mode里有一个或多个Input Source、Timer Source、Observer。

    你可以在不同Mode下运行RunLoop,以此过滤掉不需要的来源中的事件。

    分析说明:

    • 为什么要用多个Mode,就是为了不同的Mode里面的Source、Timer、Observer互不影响。
    • 典型的例子就是NSTimer在平常管用,因为主线程平常是在NSDefaultRunLoopMode(kCFRunLoopDefaultMode)的默认模式下,但在scrollview滑动的时候NSTimer就不管用了,因为scrollview滑动的时候,RunLoop是运行在UITrackingRunLoopMode模式下的。所以要想NSTimer在滑动的时候也管用,就要将NSTimer添加进NSDefaultRunLoopMode和UITrackingRunLoopMode这两个Mode下。
    • NSTimer是一种Timer Source。
    • 这里说明一下,RunLoop必须有Timer或Source才能运行,否则会退出,即使只有Observer也不行。

    Input Sources

    Input Sources以异步方式向线程传递事件。事件的来源取决于Input Sources的类型,通常为两个类型中的一个:1. 基于端口(Port)的输入源,它监视应用程序的Mach端口;2. 自定义(Custom)输入源,监视自定义事件源。

    就RunLoop来说,Input Sources是哪一种类型并无所谓。两种类型的源的唯一区别是,基于端口(Port)的输入源自动从内核发出信号,而自定义(Custom)输入源必须手动地从另一个线程发出信号。

    分析说明:

    • 这里我也一知半解,关于Mach端口可以去看这篇大神文章里的说明。至于Input Source 和Source1和Source0的关系在后面会说到。

    1. Port-Based Sources(基于端口的输入源)

    在Cocoa和Core Foundation里,提供了与端口(Port)相关的的对象和函数,你可以使用它们来创建基于端口的输入源。

    比如,在Cocoa里,你根本不必直接去创建一个端口输入源,你只需要创建一个端口对象,并使用NSPort的方法将这个端口对象添加到RunLoop中,端口对象就会自动为你处理输入源的创建和配置。

    在Core Foundation中,您必须手动创建端口及其RunLoop源。

    有关如何设置和配置基于端口的自定义源的示例,请参阅配置基于端口的输入源

    分析说明:

    • 基于端口的输入源:就是Source1。具体后面说明。

    2. Custom Input Sources(自定义输入源)

    要创建自定义输入源,必须使用 Core Foundation 里的CFRunLoopSourceRef的相关函数。

    有关如何创建自定义输入源的示例,请参阅定义自定义输入源。有关自定义输入源的参考信息,另请参阅CFRunLoopSource参考

    3. Cocoa Perform Selector Sources(PerformSelector源)

    除了基于端口的输入源(Port-Based Sources),Cocoa还定义了一个自定义输入源,允许你在任何线程去执行一个selector。

    与基于端口的源相同的是,Perform Selector请求在目标线程上被执行,从而缓解了一个线程上运行多个方法时可能会发生的同步问题。与基于端口的源不同的是,一个Perform Selector Source会在执行完后从这个RunLoop中被移除

    想要在目标线程上执行一个selector,目标线程必须有一个活动的RunLoop。主线程在应用程序启动时,已经具备了一个RunLoop;而子线程必须去你自己手动获取RunLoop,子线程的RunLoop才会存在。

    分析说明:

    • 关于RunLoop是如何获取和创建的,可以去看这篇文章

    RunLoop通过一次循环处理所有排队的Perform Selector,而不是每次循环只处理一个

    在其它线程上执行selector的方法如下

    //在主线程的下一个RunLoop的循环里,去执行selector。这两个方法可以选择是否阻塞当前线程直到这个selector被执行完毕。
    performSelectorOnMainThread:withObject:waitUntilDone:
    performSelectorOnMainThread:withObject:waitUntilDone:modes:
     
    //在任意线程中(前提是你有这个线程的对象)执行selector。这两个方法可以选择是否阻塞当前线程直到这个selector被执行完毕。
    performSelector:onThread:withObject:waitUntilDone:
    performSelector:onThread:withObject:waitUntilDone:modes:
     
    //在RunLoop的下一个循环周期和可选的延迟之后,在当前线程执行selector。因为它必须等到下一个循环去执行selector,所以这些方法提供了来自当前执行代码的自动迷你延迟。多个selector按照排队顺序执行。
    performSelector:withObject:afterDelay:
    performSelector:withObject:afterDelay:inModes:
     
    //这个是针对performSelector:withObject:afterDelay: or performSelector:withObject:afterDelay:inModes: method使用的,用来取消发送到当前线程的消息。
    cancelPreviousPerformRequestsWithTarget:
    cancelPreviousPerformRequestsWithTarget:selector:object:
    

    分析说明:

    • Perform Selector也是自定义输入源。
    • Perform Selector它是比较特殊的,也是属于Source0(非端口输入源)。

    Timer Sources(定时器源)

    定时器源在预设的时间将时间同步传递给你的线程。定时器是线程通知它自己干事情的一种方式

    虽然定时器是基于时间的通知,但是它并不是一种实时机制。与输入源类似,Timer与Mode相关联,如果Timer不在RunLoop当前所运行的Mode中,它就不会被触发(分析说明:这一点上面提过了)。还有,如果RunLoop正在执行一段程序,而这时定时器的时间到了,它也不会被触发,它会等下一次的时间点去触发。

    分析说明:

    • 假如一个定时器的触发时间是5点0秒,5点10秒,5点20秒...那么到了5点0秒的时候,假如RunLoop正在执行一大段代码,那么定时器不会被触发,它只能等5点10秒这次了。如果5点10这次也错过了,那么就等5点20秒了。

    Run Loop Observers(RunLoop的观察者Observer)

    Observer在RunLoop的特殊位置触发。可以使用observer来准备线程以处理事件,或在线程进入休眠状态之前准备线程。

    observer触发的位置(可以去看下面的那张图):

    1. 即将进入RunLoop,通知observer;
    2. 即将处理Timer,通知observer;
    3. 即将处理Source(非端口的输入源),通知observer;
    4. 线程即将休眠,通知observer;
    5. 线程刚被唤醒,但在它处理唤醒它的事件之前,通知observer;
    6. 线程退出了RunLoop,通知observer;

    你可以使用Core Foundation添加Observer到应用程序里,需使用CFRunLoopObserverRef类型。

    与Timer类似,Observer可以使用一次或重复使用。一次性Observer在触发后将其自身从RunLoop中移除,而重复的Observer会继续存在于RunLoop中,您可以指定Observer在创建时运行一次还是重复运行

    The Run Loop Sequence of Events(RunLoop的内部运行逻辑)

    每一次运行RunLoop,线程对应的RunLoop就会处理挂起的事件,并通知观察者。它执行的顺序如下

    1. 通知Observer即将进入RunLoop
    2. 通知Observer即将处理Timer
    3. 通知Observer即将处理Source0(非端口的输入源)
    4. 处理Source0(非端口的输入源)
    5. 如果有Source1(基于端口的输入源)准备就绪并等待被触发,立即处理该事件,并跳到步骤9
    6. 通知Observer即将休眠
    7. 线程休眠,直到发生以下事件之一:
      • 一个事件到达Source1(基于端口的输入源)
      • 一个定时器(Timer)触发
      • RunLoop超时
      • RunLoop被手动唤醒(例如添加一个Source0非端口的输入源)
    8. 通知Observer线程刚刚唤醒
    9. 处理待处理的事件
      • 如果用户定义的Timer触发了,则处理这个定时器事件并重新启动RunLoop循环,跳到步骤2
      • 如果输入源触发了,则传递事件
      • 如果RunLoop被手动唤醒,但尚未超时,重新启动RunLoop循环,跳到步骤2
    10. 通知Observer RunLoop已经退出。

    可以使用RunLoop对象显式唤醒RunLoop,其它事件也可能导致RunLoop被唤醒,例如添加一个Source0(非端口的输入源)会唤醒RunLoop,以便立即处理输入源,而不是等到其它事件发生

    ****注意:下图有错误,最左边应该改为Source1(port),且缺少一个超时唤醒;10应该改为通知Observer,RunLoop已经退出,而不是即将退出****。

    image
    以下是CFRunLoop的一些分析,文档中并没有,属于补充,帮助更好的理解。这里也会说明Source1和Source0。

    关于CFRunLoop

    在Core Foundation中,CFRunLoop的结构大致如下:

    struct __CFRunLoop {
        CFMutableSetRef _commonModes;     // Set
        CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
        CFRunLoopModeRef _currentMode;    // Current RunLoop Mode
        CFMutableSetRef _modes;           // Set
        ...
    };
    

    这里我们可以看到CFRunLoop里:

    • CFMutableSetRef _modes:说明一个RunLoop中有多个Mode。
    • 还有一个叫currentMode的,这就是RunLoop当前所运行的Mode,正如上文所说的,RunLoop只能指定一个Mode来运行。(补充,记住,RunLoop要想切换Mode,只能退出RunLoop,再指定一个Mode重新运行。)
    • commonModes:一个Mode可以将自己标记为“common”属性(通过使用其 Mode的Name添加到RunLoop的“commonModes”中)。主线程的 RunLoop 里有两个预置的Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个Mode都已经被标记为Common”属性。kCFRunLoopCommonModes/NSRunLoopCommonModes包含这两个Mode。
    • commonModeItems:Source/Observer/Timer都是item,你可以将source/timer/observer放入到RunLoop的commonModeItems中。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将commonModeItems里的 Source/Observer/Timer同步到具有 “Common” 标记的所有Mode里。所以说,NSTimer有两种方式去解决滑动时不运行,一种方式是上面所说,将NSTimer对象加入到kCFRunLoopDefaultMode和UITrackingRunLoopMode(或者NSRunLoopCommonModes中)中;另一种方式就是将Timer加入到顶层的RunLoop的 “commonModeItems”中。”commonModeItems” 会被RunLoop自动更新到所有具有“Common”属性的Mode里去。

    关于CFRunLoopMode

    CFRunLoopMode的结构大致如下:

    struct __CFRunLoopMode {
        CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
        CFMutableSetRef _sources0;    // Set
        CFMutableSetRef _sources1;    // Set
        CFMutableArrayRef _observers; // Array
        CFMutableArrayRef _timers;    // Array
        ...
    };
    

    看CFRunLoopMode的结构发现,Mode里面有Source(在set集合里)、Observer(array数组里)、Timer(在array数组里)。

    而根据前面的文档,我们知道RunLoop的源有Timer Source和Input Source,RunLoop还包括了Observer。这里我们对应一下,Timer Source就是CFRunLoop的Timer,Input Source就是CFRunLoop的Source,Observer是CFRunLoop的Observer。

    苹果的文档里又将Input Source分为了基于端口的输入源和自定义输入源,我们再看CFRunLoop的结构,发现里面的Source分为了Source0和Source1,那么Source0和Source1怎么区分呢?

    我去找了
    CFRunLoopSource的文档来看:

    CFRunLoopSource是RunLoop的输入源的抽象,输入源通常是异步事件。输入源在CFRunLoop里包括:CFMachPort, CFMessagePort, and CFSocket。

    CFRunLoopSource有两类:

    • 版本0(Version 0),即Source0,这样命名是因为它的上下文结构的版本字段为0。Source0在应用里必须手动管理(注意了,Source0是手动去触发的)。当一个Source0准备触发的时候,必须使用CFRunLoopSourceSignal通知RunLoop这个Source准备触发了。CFSocket是Source0。

    • 版本1(Version 1),即Source1。Source1是由RunLoop和内核管理的。当消息到达Mach端口的时候,Source1会自动发出信号。CFMachPort和CFMessagePort是Source1。

    总结一下,在前文中的Input Source又分了基于端口的输入源和自定义输入源:这里基于端口的输入源就为Source1,而自定义输入源应该就是Source0(自定义输入源需要手动从另一个线程触发)。至于performSelector比较特殊,也应该是属于Source0(非端口)的,至于内部到底是怎么实现的,我就不清楚了。

    总的来说:输入源就分为基于端口的输入源Source1和非端口的输入源Source0
    苹果文档里稍微说了一下怎么配置Source的,在文章最后。


    When Would You Use a Run Loop?(什么时候使用RunLoop?)

    1. 你需要显式运行RunLoop的唯一一种情况是,为应用程序创建辅助线程(这里的辅助线程就是指子线程)。因为应用程序的主线程的RunLoop会在应用创建的时候自动启动,所以不需要你去管主线程的RunLoop。

    2. 对于辅助线程,你需要确定是否需要RunLoop,如果是,则自行去配置并启动它。通常在所有的情况下,你都不需要去启动一个线程的RunLoop。比如你要使用一个线程去执行一个长时间运行且预定义的任务,你可以避免去使用RunLoop。

    3. RunLoop适用于这种情况:当你希望与线程进行更多的交互时。比如,如果你计划执行以下任何操作,则需要启动RunLoop:

      • 使用端口或自定义的输入源与其他线程通信
      • 在线程上使用定时器
      • 在Cocoa框架下,使用任何的performSelector方法
      • 保持线程以执行定期的任务

    如果您确实选择使用RunLoop,则配置和设置非常简单。与线程的编程一样,你应该确定在适当的时机退出RunLoop。并且,最好通过退出而不是强制终止来结束一个线程。

    分析说明:

    • 主线程对应的RunLoop是在应用创建的时候自动开启的,而子线程的RunLoop需要你手动去获取,你不去获取,子线程的RunLoop就不存在。
    • 也就是说,当你在子线程使用NSTimer的时候或者你对子线程使用performSelector系列方法时,必须先去将子线程的RunLoop开启了。
    • performSelector需要RunLoop才能有用。
    • 这里的performSelector是指上面列出的方法,应该不包括performSelectorInBackGround:withObject:,这个方法是开启一个新的子线程去执行任务。

    Using Run Loop Objects (如何使用RunLoop对象)

    • 一个RunLoop对象提供了添加Input Source、Timer Source、Observer这些主要接口。
    • 一个线程只有一个与之关联的RunLoop对象。
    • 在Cocoa中,RunLoop对象是NSRunLoop类的实例;在底层应用中,它是CFRunLoopRef类型的指针。

    1. Getting a Run Loop Object(获取RunLoop对象)

    获取RunLoop,可以使用下面的方式:

    //获得当前线程的RunLoop
    [NSRunLoop currentRunLoop];
     
    //主线程的RunLoop
    [NSRunLoop mainRunLoop];
     
    //CFRunLoop方法,获得当前现成的RunLoop
    CFRunLoopRef CFRunLoopGetCurrent(void);
     
    //CFRunLoop方法,获得主线程的RunLoop
    CFRunLoopRef CFRunLoopGetMain(void);
    

    也可以使用NSRunLoop的实例方法:- (CFRunLoopRef)getCFRunLoop; 返回一个CFRunLoopRef类型的RunLoop。

    2. Configuring the Run Loop(配置RunLoop)

    当你在子线程运行一个RunLoop的时候,你至少得添加一个Source或Timer给它,否则当你运行它时,它会立即退出,即使有Observer也不行,必须有Source或Timer

    除了配置源(Input Source和Timer)给RunLoop,你也可以配置Observer并使用它来监测RunLoop的不同执行阶段。你可以使用 CFRunLoopObserverRef 类型和 CFRunLoopAddObserver 函数去添加一个Observer到RunLoop。注意,Observer只能使用Core Foundation(CFRunLoop)的相关方法来创建,即使在Cocoa中也是这样,也就是说,NSRunLoop没有创建Observer的相关方法

    下面是一个例子:创建Observer并添加到RunLoop中,Observer用来监视RunLoop的所有活动。(例子不用深究)

     
    - (void)threadMain
    {
        // 应用程序采用垃圾回收机制,所以不需要autorelease pool
       
        //获取当前的RunLoop
        NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
     
        // 创建一个Observer,kCFRunLoopAllActivities监视所有活动
        CFRunLoopObserverContext  context = {0, self, NULL, NULL, NULL};
        CFRunLoopObserverRef    observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                kCFRunLoopAllActivities, YES, 0, &myRunLoopObserver, &context);//myRunLoopObserver是一个回调函数
       
        //如果observer存在,就将其关联到RunLoop上
        if (observer)
        {
            CFRunLoopRef    cfLoop = [myRunLoop getCFRunLoop];
            CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
        }
       
        // 创建一个定时器Timer。注意使用scheduledTimerWithTimeInterval:方法的定时器会自动添加到当前RunLoop的默认模式(kCFRunLoopDefaultMode)下。RunLoop必须有一个Source或Timer才能正常运行
        [NSTimer scheduledTimerWithTimeInterval:0.1 target:self
                    selector:@selector(doFireTimer:) userInfo:nil repeats:YES];
       
        //使用do while循环创建RunLoop的退出时机。运行RunLoop十次。
        NSInteger    loopCount = 10;
        do
        {
            // Run the run loop 10 times to let the timer fire.
            [myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
            loopCount--;
        }
        while (loopCount);
    }
    

    这段代码下面有这么一段话:当你为长期存在的线程配置RunLoop时,最好添加一个输入源(Source)来接收消息。虽然你可以只添加一个Timer定时器到RunLoop中,但是Timer一旦被触发,它通常就失效了,这会造成RunLoop的退出。如果你添加一个重复触发的定时器Timer,它会使RunLoop运行更长的时间,但是这就涉及到了定期触发定时器去唤醒线程,这实际上是另一种方式的轮询。相比之下,输入源会等待事件发生,让线程保持休眠状态

    3. Starting the Run Loop(启动RunLoop)

    只有在辅助线程(子线程)中才需要启动RunLoop。一个RunLoop必须有一个Source或Timer,如果没有,RunLoop会立即退出。

    这里有几种启动RunLoop的方式:

    • 无条件地启动
    • 设置一个超时值来启动
    • 通过特定的Mode启动

    无条件地进入RunLoop是最简单也是最不被推荐的方式。无条件地RunLoop会使线程置于永久循环之中,可以添加和删除输入源和定时器,但停止RunLoop的方式是终止它。

    最好使用第二种方式,设置一个超时值。设置一个时间值后,RunLoop将一直运行,直到事件到达或者超出时间值。如果事件到达,则将该事件分派给处理程序进行处理,然后退出RunLoop,然后,你的代码可以重新启动RunLoop以处理下一个事件。如果时间到了,你只需要重新启动RunLoop或使用这个时间去进行任何需要的内务处理。

    除了时间值,还可以使用特定的Mode去运行RunLoop。设置时间值和Mode并不互斥。Mode类型将限制事件传递到RunLoop的源类型。

    下面的代码显示了RunLoop的基本结构,实质上,你将输入源和定时器添加到RunLoop中,然后重复的调用一段程序(这里是do-while)去启动RunLoop,每次这段调用RunLoop的程序返回时,都去检查是否出现了退出该线程的条件。如果有,则不再次启动RunLoop了,将会退出线程。如果没有,再次启动RunLoop。

    - (void)skeletonThreadMain
    {
        // Set up an autorelease pool here if not using garbage collection.
        BOOL done = NO;
     
        // Add your sources or timers to the run loop and do any other setup.
     
        do
        {
            // Start the run loop but return after each source is handled.
            SInt32    result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);
     
            // If a source explicitly stopped the run loop, or if there are no
            // sources or timers, go ahead and exit.
            if ((result ** kCFRunLoopRunStopped) || (result ** kCFRunLoopRunFinished))
                done = YES;
     
            // Check for any other exit conditions here and set the
            // done variable as needed.
        }
        while (!done);
     
        // Clean up code here. Be sure to release any allocated autorelease pools.
    }
    

    补充:可以递归地运行RunLoop,也就是说,可以嵌套RunLoop。

    分析说明:

    下面是NSRunLoop中启动RunLoop的几种方法:

    //Puts the receiver into a permanent loop, during which time it processes data from all attached input sources.
    - run
     
    //Runs the loop once, blocking for input in the specified mode until a given date.
    - runMode:beforeDate:
     
    //Runs the loop until the specified date, during which time it processes data from all attached input sources.
    - runUntilDate:
     
    //Runs the loop once or until the specified date, accepting input only for the specified mode.
    - acceptInputForMode:beforeDate:
     
    

    4. Exiting the Run Loop(退出RunLoop)

    在RunLoop处理事件之前,有两种退出方式:

    • 配置RunLoop的超时值,超时就会退出
    • 使用CFRunLoopStop函数显式停止RunLoop

    如果您可以管理它,那么使用超时值肯定是首选。指定超时值可让运行循环完成所有正常处理,包括在退出之前向运行循环观察器发送通知。

    使用该CFRunLoopStop函数显式停止运行循环会产生类似于超时的结果。运行循环发出任何剩余的运行循环通知,然后退出。不同之处在于,您可以在无条件启动的运行循环中使用此技术。

    还有一种不可靠的退出方式:

    • 删除输入源和定时器,但这不是停止运行循环的可靠方法。某些系统例程将输入源添加到运行循环以处理所需的事件。因为你的代码可能不知道这些输入源,所以它将无法删除它们,这将阻止RunLoop退出。

    分析说明:

    • 退出的三种方式
        1. 超时
        1. CFRunLoopStop函数显式停止
        1. 删除Source和Timer,但这种方式不可靠

    5. Thread Safety and Run Loop Objects(线程安全和RunLoop对象)

    Core Foundation的CFRunLoop是线程安全的,可以从任何线程调用。但是,应该尽可能地在RunLoop所属于的线程中,去配置RunLoop

    Cocoa NSRunLoop类不是线程安全的。你应该在RunLoop所属于的线程中去修改RunLoop。将Source和Timer添加到属于不同线程的RunLoop可能会出现错误。

    Configuring Run Loop Sources(配置RunLoop的Source)

    以下部分显示了如何在Cocoa和Core Foundation中设置不同类型输入源的示例。

    1. Defining a Custom Input Source(定义自定义输入源,即Source0)

    这部分没看,感兴趣的人可以自行去文档中看一下。

    2. Configuring Timer Sources(配置定时器源,即Timer)

    要创建一个定时器源,需要做的就是创建一个定时器对象并将其添加到RunLoop上

    在Cocoa中,使用NSTimer类创建定时器对象,在Core Foundation中使用CFRunLoopTimerRef类型。NSTimer类只是对Core Foundation的扩展。

    创建NSTimer对象的类方法有如下两类:

    第一类:

     + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
     + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
     + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block ;//iOS10以后新加的方法
    

    上面这类方法在创建NSTimer对象后,会自动添加到RunLoop的默认Mode(NSDefaultRunLoopMode/kCFDefaultRunLoopMode)中

    第二类:

    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
     + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
     + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block ;//iOS10以后新加的方法
     
    

    上面这类方法在创建了NSTimer对象后,必须手动添加到RunLoop的Mode中,使用addTimer:forMode:方法。你可以选择Mode的类型,Mode的默认类型还是其他类型。

    注意一点:定时器必须添加到RunLoop中才能够使用

    例子:使用NSTimer创建和调度计时器

    NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
     
    // 创建并调度第一个定时器
    NSDate* futureDate = [NSDate dateWithTimeIntervalSinceNow:1.0];
    NSTimer* myTimer = [[NSTimer alloc] initWithFireDate:futureDate
                            interval:0.1
                            target:self
                            selector:@selector(myDoFireTimer1:)
                            userInfo:nil
                            repeats:YES];
    [myRunLoop addTimer:myTimer forMode:NSDefaultRunLoopMode];
     
    // 创建并调度第二个定时器
    [NSTimer scheduledTimerWithTimeInterval:0.2
                            target:self
                            selector:@selector(myDoFireTimer2:)
                            userInfo:nil
                            repeats:YES];
    

    例子:使用Core Foundation创建和调度计时器

    CFRunLoopRef runLoop = CFRunLoopGetCurrent();
    CFRunLoopTimerContext context = {0,NULL,NULL,NULL,NULL};
    CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault,0.1,0.3,0,0,
                                            &myCFTimerCallback,&context);
     
    CFRunLoopAddTimer(runLoop,timer,kCFRunLoopCommonModes);
     
    

    3. Configuring a Port-Based Input Source(配置基于端口的输入源,即Source1)

    Cocoa和Core Foundation都提供了基于端口的对象,用于线程之间或进程之间的通信。以下部分介绍如何使用多种不同类型的端口设置端口通信。

    下面的我也没有认真去研究,只是大致看了一下。

    3.1 Configuring an NSMachPort Object(配置NSMachPort对象)

    要与NSMachPort对象建立本地连接,要创建端口对象并将其添加到主线程的RunLoop中。启动子线程时,将同一对象传递给线程的入口点函数(entry-point function)。子线程可以使用相同的对象将消息发送回主线程。

    3.1.1 Implementing the Main Thread Code(实现主线程)

    以下代码为:在主线程里,启动子线程。

    - (void)launchThread
    {
        NSPort* myPort = [NSMachPort port];
        if (myPort)
        {
            // This class handles incoming port messages.
            [myPort setDelegate:self];
     
            // Install the port as an input source on the current run loop.
            [[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];
     
            // Detach the thread. Let the worker release the port.
            [NSThread detachNewThreadSelector:@selector(LaunchThreadWithPort:)
                   toTarget:[MyWorkerClass class] withObject:myPort];
        }
    }
    

    ....

    3.1.2 Configuring an NSMessagePort Object(配置NSMessagePort对象)
    4. Configuring a Port-Based Input Source in Core Foundation(在Core Foundation中配置基于端口的输入源)

    到这里就结束了,以上的从 3. Configuring a Port-Based Input Source(配置基于端口的输入源,即Source1)开始,都是怎么配置Source的,感兴趣的自己去看吧。

    如有错误,烦请指正,谢谢!

    相关文章

      网友评论

          本文标题:看苹果官方文档怎么说RunLoop

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