Rdd7

iOS Dev Blog

iOS开发者,就职于网易,正在努力丰富iOS技能与经验.


Run loop基础概念整理

因为工程原因,恶补了run loop的一些知识,参考Apple官方文档和部分翻译,现整理出来当做巩固和日后参考。

参照Apple官方文档的介绍,run loop是与线程紧密联系在一起的,一个线程将会有一个run loop,而一个run loop也仅对应一个线程。一个run loop是一个事件处理的循环,用来调度工作和处理输入事件。Run loop的作用是当有事件需要处理时保持线程的执行以及当前无事件时休眠线程。

通俗而言,run loop使线程能够不断地处于“接受事件和消息 -> 唤醒线程并调度处理 -> 无事件休眠并等待新事件与消息”的循环中,使线程能够不断执行和接受新的事件而不至于退出。

在iOS/OSX中,可以实现run loop的有两个对象:NSRunloopCFRunloop,这两个对象都可以实现对事件与消息的等待和处理,但是需要指出的是run loop的loop部分,需要自己手工实现,即需要自己使用恰当的循环控制语句,在语句内调用run loop对象的run系列方法,从而实现完整的run loop

Run loop源

Run loop接收事件与消息通过两大类源:输入源(Input Source)定时源(Timer Source)

输入源(Input Source)

输入源传递异步事件,输入源包括基于端口的输入源和自定义输入源。基于端口的输入源由系统内核发送,自定义输入源则需要人为从其他线程进行发送。但对于run loop而言它并不关注输入源的具体来源。

基于端口的输入源

Cocoa和Core Foundation可以使用端口相关的对象和函数来创建的基于端口的源。

在Cocoa中你可以创建端口对象,并使用NSPort将该端口加入到run loop中。

在Core Foundation,需要手工创建端口和run loop源。

两种情况下,你都可以使用端口相关的函数(CFMachPortRefCFMessagePortRefCFSocketRef)来创建合适的对象。

自定义输入源

为了创建自定义输入源,你必须使用Core Foundation的CFRunLoopSourceRef类型相关的函数来创建。你可以配置一个自定义输入源通过使用若干个回调函数。Core Fundation会在配置源的不同地方调用回调函数,处理输入事件,在源从run loop移除的时候清理它。

除了定义在事件到达时自定义输入源的行为,你也必须定义消息传递机制。源的这部分运行在单独的线程里面,并负责在数据等待处理的时候传递数据给源并通知它处理数据。消息传递机制的定义取决于你,但最好不要过于复杂。

Cocoa的Selector源

除了基于端口的源,Cocoa也定义了自定义输入源,使得你可以在任意线程执行Selector。与基于端口的输入源类似,Selector的执行请求也被序列化在了目标线程,减轻很多多线程执行多个方法引起的同步问题。一个Selector执行完后会将自己从run loop移除。

Run loop每次循环都会将队列中所有的Selector都执行完,而不是一次循环只处理一个Selector。与基于端口的输入源不同的是

值得注意的是,当在非主线程,例如自己创建的线程执行Selector时,线程必须要有一个活动的run loop,否则Selector将会一直等待而不被执行直到你显式地开始了这个线程的run loop。对于主线程,由于系统一开始便开始了主线程的run loop,所以当程序开始调用applicationDidFinishLaunching:时,你已经可以开始在主线程做任何事件的调用了。

顺手翻译官方文档对Perform Selector方法调用的描述:

  • performSelectorOnMainThread:withObject:waitUntilDone:
    performSelectorOnMainThread:withObject:waitUntilDone:modes:
    在主线程调用指定的Selector,将会在主线程的下一次Runloop循环中执行Selector,这系列方法也给了你选项选择是否阻塞当前线程直到Selector被执行完毕

  • performSelector:onThread:withObject:waitUntilDone:
    performSelector:onThread:withObject:waitUntilDone:modes:
    在目标线程调用指定的Selector(前提是你有目标线程的NSThreadobject对象)。这系列方法也给了你选项选择是否阻塞当前线程直到Selector被执行完毕

  • performSelector:withObject:afterDelay:
    performSelector:withObject:afterDelay:inModes:
    调用指定的Selector在当前线程,将会在当前线程的下次run loop循环中被执行,如果设置了延迟时间,将会在对应时间后被执行。如果调用的是指定modes的方法,将会只在指定modes集合中的run loop执行此方法(注意modes至少要包含一个mode String,如果为nil或者空数组,将会直接返回而不执行),如果调用的是无modes的方法,将会在默认模式执行。由于Selector将会在run loop的下次循环去执行,这系列方法默认了微小的延迟在调用之后。与上文说的一样,这些Selector将会被序列化而一个接着一个被执行以确保他们是串行的

  • cancelPreviousPerformRequestsWithTarget:
    cancelPreviousPerformRequestsWithTarget:selector:object:
    允许你取消当前线程中指定的Selector(如果有参数,参数也需要一致,否则将取消失败)

定时源(Timer Source)

定时源将在指定的时间传递同步事件,然而需要指出的是它并非是实时的,当定时器到了指定时间触发时,如果run loop正在处理其他事件,将会一直等到下次run loop循环才会触发,这中间便产生了微小的误差。而定时源同样也需要指定run loop模式,在非定时器指定模式下运行的run loop将不会触发执行定时源的事件。

如果定时器设置为重复执行,那么需要知道如果定时器被延迟以至于他错过了一个或多个触发时间,他将会在最近的run loop事件处理时间点触发,而后按照之前正常的触发时间点触发。简言之,定时器的触发时间点在设置后不会因为被延迟而改变,若错过就错过,只会立即补一次触发后照样按照老的安排时间触发

Run loop 观察者

Run loop也能被观察,在run loop执行的特定阶段将会触发特定的观察事件。Run loop观察者可以与以下run loop事件关联:

  • Run loop入口
  • 当run loop 将要处理一个定时器
  • 当run loop 将要处理一个输入源
  • 当run loop 将要进入休眠状态
  • 当run loop被唤醒后,但在处理将它唤醒的事件之前的这个时间状态
  • 当run loop退出

通过创建CFRunLoopObserverRef类型实例,你可以新增run loop观察者,它将会保持追踪你的自定义回调函数和你感兴趣的run loop事件。

Run loop观察者可以被仅使用一次,也可以被多次重复适用,这与定时器类似。单次的run loop观察者将在自己被执行(对应的run loop事件发生,产生回调)后将自己从run loop移除;而重复的观察者将会持续依附在run loop内。在创建run loop观察者时你需要指定他是一次的还是重复的。

Runloop 事件队列

每次运行run loop,它都会处理未处理的事件和产生通知给任何需要通知的观察者。Run loop事件执行的顺序如下:

  1. 通知观察者run loop已经启动
  2. 通知观察者即将要触发已准备好的定时器
  3. 通知观察者即将触发任何非基于端口的源
  4. 触发任何已经已经准备好的非基于端口的源
  5. 如果有基于端口的源已经准备好并等待被触发,立即处理此事件,并跳转到步骤9
  6. 通知观察者线程即将进入休眠
  7. 将线程休眠直到如下任一事件的发生:

    • 某个事件到达基于端口的源
    • 一个定时器触发了
    • Run loop 设置的事件超时
    • Run loop被显式的唤醒
  8. 通知观察者线程刚被唤醒
  9. 处理未处理的事件:
    • 如果用户定义的定时器被触发,处理对应的定时事件并重启run loop,进入步骤2
    • 如果一个输入源被触发,传递对应的消息
    • 如果run loop被显式唤醒但未超时,重启run loop,进入步骤2
  10. 通知观察者run loop退出

Run loop 模式

在对上面的各种概念有了了解之后,就可以较清晰的理解run loop的模式(Mode)。一个线程的run loop会有一个或多个模式,每次run loop进行一次循环,都要指定其运行在哪个具体的模式下。一个模式包含了所有对应模式下要监视的输入源,定时源和观察者,在run loop运行在某个具体模式下时,只有这个模式下相关的输入源和定时源才会被处理,这个模式下的观察者才能收到通知;与这个模式不关联的源的处理将处于暂停状态,不关联的观察者也将不会收到通知。

在Cocoa和Core Foundation中,已经为我们定义了一个默认的模式和一些常用的模式,要指定某个具体的模式,可以通过对应模式的名字的字符串来标识,当然也可以通过自己指定一个新名字来创建自定义的模式,并在其中添加输入源,定时源和观察者。

参照Apple文档,翻译并列举常用的模式:

  • 模式 Mode:Default
    名称 Name:NSDefaultRunLoopMode(Cocoa) / kCFRunLoopDefaultMode(Core Foundation)
    描述 Description:默认模式是最常用的模式。通常情况下,你应该使用这个模式去启动你的run loop并配置输入源

  • 模式 Mode:Connection
    名称 Name:NSConnectionReplyMode(Cocoa)
    描述 Description:Cocoa使用这个模式去监视等待NSConnection对象的回复,一般很少用到这个模式

  • 模式 Mode:Modal
    名称 Name:NSModalPanelRunLoopMode(Cocoa)
    描述 Description:Cocoa使用此模式去区分是否是Model panels的事件

  • 模式 Mode:Event tracking
    名称 Name:NSEventTrackingRunLoopMode(Cocoa) / UITrackingRunLoopMode(iOS)
    描述 Description:当类似鼠标拖动或其他一系列需要追踪用户界面时,Cocoa使用这个模式去限制输入事件的处理,保证UI的流畅性

  • 模式 Mode:Common modes
    名称 Name:NSRunLoopCommonModes(Cocoa) / kCFRunLoopCommonModes(Core Foundation)
    描述 Description:这个mode实际上是一个常用mode的集合.将一个输入源与这个模式关联意味着这个输入源将与所有这个模式组包含的模式相关联。对于Cocoa的应用程序,Common模式默认将包含default, modal和event tracking三种模式,而对于Core Foundation程序则默认只包含default.你可以将自定义的模式加入到Common模式组通过CFRunLoopAddCommonMode

附上一张run loop模式结构图:

参考资料

  1. Apple Documents - Run loops
  2. iOS多线程编程指南(三)Run Loop
最近的文章

Swift-String

更新于2016.07.13,当前Swift版本Swift 2.3,至少支持到Swift 3.0 (3.0文档可供参考)万恶的起源:扩展字形集群 Extended Grapheme ClustersSwift中每个字符(Character)都是一个扩展字形集群(Extended Grapheme Clusters),由一个或若干个Unicode标量(Unicode Scalar)排列组成。抛弃以上专业的说法,先看如下代码,再进行通俗理解://代表国籍的Unicode Scalars :...…

继续阅读
更早的文章

使用IBDesignable和IBInspectable构建自定义UI控件

怎么突然弄起这个了之前习惯了代码+frame布局,突然觉得是时候征服一下StoryBoard、Xib和自动布局了。偶然搜索到CocoaChina上一篇去年的文章(如何在iOS 8中使用Swift和Xcode 6制作精美的UI组件),居然可以用IBDesignable和IBInspectable构建直接在StoryBoard和Xib上显示和编辑的控件!(原谅我见识浅薄)顿时眼前一亮,于是自己也跟着教程学了一些基础。What’s thisIBDesignable:使用IBDesignable去...…

继续阅读