RunLoop问题集

作者: Delpan | 来源:发表于2017-05-02 21:18 被阅读1512次

    什么是RunLoop?

    答:RunLoop是线程相关的基础框架中的一部分,是一个事件处理对象,每一个线程都有与之对应的RunLoop,但并不是线程创建时就有RunLoop,只有当前线程第一次主动获取RunLoop,系统才会创建当前线程相应的RunLoop。

    RunLoop的作用是什么?

    答:1).管理线程的生命周期及活动。                                           

         2).处理输入事件源以及通知观察者。

    如何使用RunLoop?

    答:iOS/OSX提供了Core Foundation(CFRunLoopRef)和Cocoa(NSRunLoop)两套API来使用RunLoop,可以通过CFRunLoopGetMain() 和 CFRunLoopGetCurrent()或[NSRunLoop mainRunLoop]和[NSRunLoop currentRunLoop]来获取RunLoop对象,NSRunLoop是基于CFRunLoopRef的高层组件,CFRunLoopRef的API都是线程安全的,但NSRunLoop的API不是线程安全的。虽然CFRunLoopRef的操作都是线程安全的,但不建议跨线程处理。

    如何启动RunLoop?

    答:可以通过NSRunLoop对象的run,runUntilDate:,runMode:beforeDate:方法,或通过CFRunLoopRef的CFRunLoopRun(),CFRunLoopRunInMode()来启动RunLoop。

    使用NSRunLoop和CFRunLoopRef来启动RunLoop有何不同?

    答:虽然NSRunLoop是基于CFRunLoopRef的高层组件,对NSRunLoop的操作最终都会转换成对CFRunLoopRef的操作,但需要注意的是,NSRunLoop与CFRunLoopRef并不是简单的接口转换,就像启动RunLoop一样,NSRunLoop的Run方法与CFRunLoopRun()并不是对应着的,而且有着一定的区别。

    NSRunLoop Run CFRunLoopRun()

    如何退出RunLoop?

    答:退出RunLoop的方式有三种,分别是:方式一.给RunLoop设定超时时间;方式二.使用CFRunLoopStop()函数显式退出;方式三.移除CurrentMode的所有输入源和定时源。

    方式一是官法推荐的方式,可以安全有效地退出RunLoop。方式二,用CFRunLoopStop()函数并不是绝对可以退出Run的,要看以什么方式启动RunLoop,如果用[[NSRunLoop currentRunLoop] run]来启动RunLoop,那么用CFRunLoopStop()是无法退出RunLoop的,正如上面的伪代码所示,以[[NSRunLoop currentRunLoop] run]启动的RunLoop,唯一退出的方式是移除CurrentMode的所有输入源和定时源,也就是方式三,但这种方式是不稳定的,因为系统会添加一些输入源或定时源来完成一些操作。

    RunLoop是否自动运行的?

    答:所有线程的RunLoop都是默认不启动的,但主线程的RunLoop会随着应用的运行而被启动。

    RunLoop处理的输入事件源有哪些?

    答:RunLoop处理的事件源有两种,分别是输入源(Input Source)和定时源(Timer Source),而主线程的Main RunLoop还会处理GCD事件源。

    什么是RunLoopMode?

    答:RunLoop可以有多个RunLoopMode,RunLoopMode包含了输入源(Input Source),定时源(Timer Source)和观察者(Observer)。

    RunLoop每次进入Run时都需要指定一个RunLoopMode,指定RunLoopMode后,只有当前RunLoopMode内的源和观察者会被处理,其它的源和观察者需要等到RunLoop运行其RunLoopMode时才会被处理,切换RunLoopMode的唯一方式是,退出当前RunLoopMode,重新指定一个RunLoopMode进入Run。当RunLoop选择一个RunLoopMode进入Run时,若这个RunLoopMode中并没有需要处理的源(输入源或定时源),RunLoop就会直接退出。

    RunLoopMode

    RunLoopMode中的_timerPort是_timers中所有定时源的公共Port,_portToV1SourceMap记录了_sources1以及对应的Port,通过Port获取相应的Source1。

    通过Port获取Source1

    什么是CommonMode和CommonModeItem?

    答:CommonModeItem是公共ModeItem,CommonMode是公共RunLoopMode,当把一个RunLoopMode注册为CommonMode时,CommonModeItem被会自动添加到CommonMode里,当CommonModeItem有所改动时,CommonMode也会作出相应的改动。

    什么是定时源(Timer Source)?

    答:用于延时或重复的时间间隔处理事件。

    如何使用定时源?

    答:CFRunLoopTimerRef是RunLoop中唯一的定时源,以定时器的形式表示和使用,可选择的定时器有,NSTimer,CADisplayLink,CFRunLoopTimerRef,GCD Timer。

    四种定时器的区别是什么?

    答:NSTimer和CADisplayLink是Cocoa提供的高层定时器,CFRunLoopTimerRef是Core Foundation提供的基础定时器,NSTimer和CADisplayLink是建立在CFRunLoopTimerRef之上的高层组件,而CFRunLoopTimerRef是建立在mk_timer之上。NSTimer和CADisplayLink主要区别在于信号的发射频率不同,CADisplayLink的信号发射频率固定在16.67ms一次,而NSTimer的信号发射频率可自由定义(具体请看iOS10定时消息的改动)。

    GCD Timer调用栈

    GCD Timer有别于前三种定时器,是由GCD系统所管理的定时器,通过一定的时间间隔dispatch任务到相应的队列中处理,以主线程为例,时间间隔到达后,GCD系统将Block dispatch到主线程对应的Main Queue,等待Main RunLoop检测和处理。

    如何选择使用哪一种定时器?

    答:除非需要实现定时动画,否则不建议使用CADisplayLink作为定时器(具体请看iOS10定时消息的改动。什么是定时动画?请查看iOS动画的基础知识);NSTimer适用于大部份情况,但需要注意循环引用的问题;GCD Timer的缺点在于,不能在自己所创建的子线程中使用。

    CFRunLoopTimerRef的触发原理是怎样的?

    答:具体请看iOS10定时消息的改动

    什么是输入源?

    答:是RunLoop所处理的事件源之一,主要用于线程或进程交互,输入源分为基于端口输入源(Source1)和非端口输入源(Source0)。

    基于端口输入源(Source1)与非端口输入源(Source0)的区别是什么?

    答:1).Source0与Source1都是CFRunLoopSourceRef类型,但配置方式不同,Source0用CFRunLoopSourceContext来配置,Source1用CFRunLoopSourceContext1来配置。

         2).Source0与Source1都可用于线程(或进程)交互,但交互的形式有所不同,Source1监听端口,当端口有消息到达时,相应的Source1就会被触发回调,完成相应的操作;而Source0并不监听端口,让Source0执行回调需要手动标记Source0为待处理状态,还需要呼醒Source0所在的RunLoop。

    Source1交互 Source0交互

          3).从Source0与Source1的交式方式了解到,Source1的交互会主动呼醒所在的RunLoop,而Source0的交互则需要依赖其它线程来呼醒Source0所在的RunLoop。

          4).一次Loop只能执行一个Source1的回调,但一次Loop可以执行多个待处理的Source0的回调。

    如何创建Source0?

    创建Source0

    如何标记Source为待处理状态,且呼醒所在的RunLoop?

    Source0交互

    如何创建Source1以及如何交互?

    答:有Cocoa和Core Foundation两套API来配置和使用Source1;Cocoa有NSPort,NSMachPort,NSMessagePort,NSSocketPort等类,Core Foundation有CFMachPortRef,CFMessagePortRef,CFSocketRef等。其中用得比较多的是NSMachPort和CFMessagePortRef。

    CFMessagePortRef NSMachPort

    Cocoa所提供的类只是建立在Core Foundation之上的高层组件,且提供了toll-free bridged。需要注意的是,NSMachPort接收和发送需要是同一个对象;CFMessagePortRef接收和发送的Port所用的name要相同,CFMessagePortSendRequest()函数通过CFMessagePortRef的name来查找相应的接收端口来进行消息发送(不建议直接使用mach_msg()来发送消息,关于Port可以查看Inter-Process Communication)。

    RunLoop的内部逻辑是怎样的?

    Run内部逻辑 官方文档的Run内部逻辑

    需要注意的是第五步,官方文档写的是如果有基于端口的输入源待处理,就进入第九步,这跟CFRunLoop的源码不同。

    CFRunLoop Run源码第五步

    从源码可以看到,第五步检测的是GCD端口事件,而不是官方文档所写的基于端口的输入源,但经过大量测试发现,第五步实际上会检测所有未处理的端口事件,而并非像官方文档或源码所展示的那样(太坑爹了,居然官方文档跟源码不同,源码又和实际测试结果不同😂)。

    如果有什么地方写错的麻烦指出,如果有什么还想知道的请在评论留言,我会尽快补上的。

    相关文章

      网友评论

      • iOS开发随笔:NSTimer适用于大部份情况,但需要注意循环引用的问题;GCD Timer的缺点在于,不能在自己所创建的子线程中使用。
        GCD Timer和runloop没有关联,在子线程也可以使用吧?dispatch _source _t不能使用嘛?这点上有点不理解,求指点
        Delpan:@iOS开发随笔 GCD Timer能不能跟目标线程交互,主要看目标线程是否有RunLoop,GCD队列跟线程的交互是通过向目标线程的RunLoop Port提交任务,目标RunLoop发现事件队列有未完成任务,就去完成,但GCD系统所创建的线程不是由Application来控制的,通过GCD的API向相关的队列添加任务,GCD再把任务发到空闲或指定的线程(例如:Application的Main Thread)中,再做处理,不管怎么样,如果你打算创建一个独立线程(pthread或NSThread),用GCD怎么向这个线程提交任务?
      • iStig:RunLoop每次进入Run时都需要指定一个RunLoopMode,指定RunLoopMode后,只有当前RunLoopMode内的源和观察者会被处理,其它的源和观察者需要等到RunLoop运行其RunLoopMode时才会被处理,切换RunLoopMode的唯一方式是,退出当前RunLoopMode,重新指定一个RunLoopMode进入Run。当RunLoop选择一个RunLoopMode进入Run时,若这个RunLoopMode中并没有需要处理的源(输入源或定时源),RunLoop就会直接退出。
        请问下runloop 切换 runloopmode时 是否表示着 同时进入另一个 runloop 周期去切换的?
        Delpan:@iStig 需要加一层处理才能在RunLoop退出之后进入runloopmode2
        iStig:如果上面的猜测成立 再问一下最后一句话
        “若这个RunLoopMode中并没有需要处理的源(输入源或定时源),RunLoop就会直接退出。”
        请问 当有2个runloopmode 分别是runloopmode1 runloopmode2 ,先是runloopmode1执行 但是并没有需要处理的源 这时候runloop就会退出 那么runloopmode2是否就不会被runloop执行了
        Delpan:是的,必须先退出当前周期才能重新选择新的runloopmode
      • 爱上我们的微笑:接着yy的文章看,每次都感觉你们是承前启后的想法,😂
        Delpan:@爱上我们的微笑 都有互相学习的吧,而且每个人在研究知识点的时候,想到的问题都不太一样。
      • XIAODAO:“输入源分为基于端口输入源(Source1)和自定义输入源(Source0)” ,官方文档里,输入源分三种:基于端口的源、自定义源和Perform Selector源。请问Perform Selector源归类到Source1还是Source0?
        XIAODAO:@Delpan 在Perform Selector源里,对于主线程 performonMainThreadXXX 和 指定线程 performonThreadXXX 这两种情况,创建的是source0任务。而对于当前线程 performSelector:withObject:afterDelayXXX 这种情况,则不是source0而是timer。
        Delpan:@XIAODAO perform执行selector有些特殊,并不是指定source来完成的,有些是先通过定时器来完成的,总的来说,最后都回到soucre0,所以就纳入了自定义源范围
        XIAODAO:我觉得还是按照官方说法靠谱:输入源分为基于端口输入源(Source1)和非基于端口输入源(Source0)
      • 佛前一粒沙:大屌哥!
      • 我叫阿水:斌哥,最后说经过大量测试,能说下怎么测试吗:no_mouth:
        我叫阿水:@Delpan 测试的时候,能打断点跳到runloop的源码那里?
        我叫阿水:@Delpan 用source1,怎么让端口持续的发消息?弄个定时器?:joy:
        Delpan:@作为一个饲养员的快乐 你为什么知道我叫斌...
        就是用定时器先触发,或者用Source1触发,再观察整个过程是怎样的
      • upcode:博主好。拜读了您的这篇文章,还有前面两篇关于页面跳转的性能优化,受益颇多,但还是有一些云里雾里的,有些问题望您解惑。
        1、“页面优化二”中 这么说道“每次Loop都会有两次检测是否有端口事件需要处理的机会,但是一次Loop只有一次机会处理端口事件,即在步骤5或步骤7触发处理端口事件”,仔细读这句话,步骤5、7应该是在做检测动作而不是处理动作吧?

        2、在步骤图中,没看到处理定时器的步骤,定时器是在哪里处理的?

        3、图中步骤三、四是处理了非端口事件源,那端口事件源是在哪里处理?

        4、结合页面跳转优化的两篇文章,渲染任务是几时被提交,还有几时被渲染的?两次loop是怎么衔接执行的呢?

        如果可以,希望博主可以在步骤图中的每一步后面标明一下该步具体做了什么动作,我想这样会让读者对前两篇的文章理解得更加透彻。
        Delpan:@xzhuang 1.对,每次Loop只会在第9步来处理端口事件,第5,7步都会检测是否有端口事件,如果有就会到9步处理。
        2.定时源和输入源都是端口事件源,所有检测都在第5,7步。
        3.第5,7步检测定时源和输入源,在第9步处理
        4.渲染任务在第2,6步发出,发到渲染进程后由渲染进程完成接下来的工作,你的Application只同步提交任务过去就可以了
      • 07aa06afafd8:你好,请问下载了Core Foundation源码之后,如何编译运行?
        Delpan:@iqRocket 自己在那个工程加个Target就可以了
        07aa06afafd8:@Delpan 但在 https://opensource.apple.com/source/CF/CF-1153.18/ 这里面下载下来的源码不是没有工程的吗?把这些源码拉到一个工程也编译不过。
        Delpan:@iqRocket 工程只是缺少了Target,加一个就好了
      • XIAODAO:GCD Timer 是基于 MKTimer(mach kernel timer),是由 mk_timer 驱动。而且GCD Timer 是不依赖 RunLoop 的
        Delpan:GCD Timer依不依赖RunLoop,应该要看是怎么交互。是由 mk_timer 驱动,但也得提供线程处理,线程由GCD系统管理,最后GCD再把消息怎么发,就看你怎么用了。
      • XIAODAO:你好,“CFRunLoopTimerRef是RunLoop中唯一的定时源”,说法感觉有些怪。觉得“Runloop中定时源的底层基于CFRunLoopTimerRef”说法更妥
        Delpan:谢谢你的建议,我的语文水平不太好,表达能力不强😂
      • ymhlbj:只能膜拜了
      • 老司机Wicky:想问一下,5跳9那句为什么不一样啊:scream:我看觉得是直接跳转过去的啊。。
        Delpan:@老司机Wicky 但是这跟文档和源码不符,实际测试的结果是所有端口源都会跳到第九步
        老司机Wicky:@Delpan 在我看来,里面那层判断就是在判断基于端口的源事件:stuck_out_tongue_winking_eye:
        Delpan:跳是跳过去,但是判断的源不同
      • 0ae8e443fa6e:先赞后看。这段时间写了点swift,公司这边还要我学点java帮打下手,对iOS这块的基础知识掌握得都不牢固了啊...
      • LLVKS:表哥好
        LLVKS:@Delpan 大表哥
        Delpan:@LLVKS 你好
      • sindri的小巢:特来膜拜表哥
        Delpan:@sindri的小巢 谢谢骑神
        ConstantCody:抢了骑神的前排:smirk:
      • ConstantCody:大表哥实力圈粉!弱弱地问一句,你是怎么学习runloop的呢?看啥资料?
        Delpan:@ConstantCody 看官方文档,看源码,做测试啊

      本文标题:RunLoop问题集

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