Run Loops 详解

官方文档

Run loops 是基础框架中和线程相关的的部分,Run loop是一个事件处理循环,用来安排工作和协调事件。Run loop的作用是让线程该忙的时候忙,该休眠的时候休眠。

Run loop 的管理不是全自动的。你需要在合适的时机去手动启动Run loop 来响应相应的事件。Cocoa(NSRunloop) 和 Core Fundation(CFRunloopRef)框架都提供了Run loop对象来帮助你配置和管理你的线程中的Run loop。你不需要显式的去创建Run loop对象,包括主线程在内的所有线程都有一个对应的Run loop 对象。不过只有子线程需要显式的启动他们的Run loop,子线程的Run loop默认是不启动的,需要手动去开启。作为App启动工作的一部分,应用程序框架会自动启动并运行主线程中的Run loop。

关于Run loop和它们的配置,接下来会有更详细的说明,另外你也可以从NSRunLoop Class Reference和CFRunLoop Reference中获取更多的细节信息。

解析 Run Loop

它是一个让线程来响应事件的循环。你的代码中提供控制语句,用来实现run loop的真正的循环,换句话说,你用 while和for循环来驱动run loop。在这个循环内,用run loop 对象来运行事件处理的代码。

Run loop 接收2种事件来源,1、输入(Input source)传递过来的异步事件,一般是别的线程或者别的程序发送的。2、定时器(Timer source)传递同步事件,在特定的时间触发或者按间隔重复。事件触发时,都由系统特定的处理模块来处理。

图3-1表示的是run loop和各种事件源的概念图,Input Source事件传递一步时间给相应的句柄,因为runUntilData:方法退出Run loop。定时器传递过来的事件不会导致run loop退出。

3-1

另外处理Input source事件时,Runloop 同时会发送一些通知。注册了这些通知的观察者会收到通知,然后在线程中做相应的处理。在线程中,用Core Foundation 来注册这些观察者。

接下来的内容会更多的讲述run loop的组件和Modes、处理事件时的通知。

Run Loop Modes

run loop mode 是一组Source 、Timer和Observer。每当你运行run loop时,需要显式或者隐式地指定一个mode。在这个期间,只有和此mode相关的sources、obvservers会被监测和通知。和其他mode相关的sources和observers则等待合适的mode。

你可以通过name来定位mode。在Cocoa和Core Foundation中定义了一个默认的Mode和一些常用的Mode,这些Mode可以通过字符串name来指明。你也可以用自定义的字符串name来定义一个自定义的Mode。虽然给自定义mode的名字是可以随便取的,但是他们的内容却不是随意的,你必须确定加入run loop的sources、timers、observers是有用的。

你通过mode在一堆你不想要的sources中来过滤筛选有用的,大多数时间你在系统的默认mode中运行run loop。但是模态面板(modal panel)可以运行在 “模态”模式下。在这种模式下,只有和模态面板相关的源可以传递消息给线程。对于次线程,可以使用自定义模式处理时间优先的操作,即屏蔽优先级低的源传递消息。

注:Mode的区分基于事件的来源,而不是事件的种类,举个例子,你不会只用mode来匹配鼠标点击事件和键盘事件。你应该用mode来监听一组不同的端口,暂时暂停定时器,或者改变sources和observes。

表3-1 列出的是Cocoa和Core Foundation的标准Modes,并标注了你该什么时候使用它们,name这一列列出了你在代码中实际使用的名字。

Table 3-1  Predefined run loop modes
Mode Name Description
Default NSDefaultRunLoopMode(Cocoa)

kCFRunLoopDefaultMode (Core Foundation)

The default mode is the one used for most operations. Most of the time, you should use this mode to start your run loop and configure your input sources.
Connection NSConnectionReplyMode(Cocoa) Cocoa uses this mode in conjunction with NSConnection objects to monitor replies. You should rarely need to use this mode yourself.
Modal NSModalPanelRunLoopMode(Cocoa) Cocoa uses this mode to identify events intended for modal panels.
Event tracking NSEventTrackingRunLoopMode(Cocoa) Cocoa uses this mode to restrict incoming events during mouse-dragging loops and other sorts of user interface tracking loops.
Common modes NSRunLoopCommonModes(Cocoa)

kCFRunLoopCommonModes (Core Foundation)

This is a configurable group of commonly used modes. Associating an input source with this mode also associates it with each of the modes in the group. For Cocoa applications, this set includes the default, modal, and event tracking modes by default. Core Foundation includes just the default mode initially. You can add custom modes to the set using the CFRunLoopAddCommonMode function.

1、Default(Cocoa,CF) :default mode是使用最多的一个mode,大多数时间,你用这个mode来启动runloop 和配置input sources。
2、Connection(Cocoa):Cocoa 用这个来关联NSConnection对象,以监听回复,你应该很少有机会用到这个Mode。
3、Modal(Cocoa):Cocoa用这个mode来识别Modal panels的事件。
4、Event tracking(Cocoa):在鼠标拖拽(手指拖拽)之类的跟踪循环中,Cocoa用这个mode来排除其他事件的干扰。
5、Common modes(Cooca,CF):这是一组可配置的常用modes,和这个mode相关的 input source同时也和这个组中的其他modes相关。

对Cocoa程序来说,Common modes 默认包括default,modal,event tracking。Core Foundation 则默认在初始化时只包含default mode,你可以用CFRunLoopAddCommonMode方法添加自定义mode到这个组中。

Input Sources

Input Sources 以异步的方式传递事件到线程中,事件的source取决于input source的类型,一般有一到两种。基于端口的input source监控着程序的Mach端口。自定义的input source监控着自定义事件source。至于run loop,它不关心Input  source的种类。系统会实现这2种方式供你使用。唯一的区别是发送的方式,基于端口的source由系统内核自动发送,自定义source则必须由其他线程手动发送。

当你创建一个input source时,你把它分配给个run loop中的一个或多个mode。mode会在特定的时间影响监听的input source。多数时间里,你以默认的方式运行run loop,不过你也可以用自定义的方式运行。如果一个input source不在当前mode中,所有他产生的事件都会阻塞,直到它运行在正确的mode中

接下来的内容描述了一些input source

基于端口的sources

Cocoa和Core foundation 为创建基于端口的input source 提供了端口相关的对象和方法。
举个列子,在Cocoa中,你永远不需要直接创建input source你仅需要创建一个端口对象,然后用NSPort的方法把它添加到run loop中。端口对象会处理创建和配置input source。

在Core Foundation中,你必须手动创建端口和source。
在上面两种方式中,你都需要用和端口相关的方法和类型(CFMachPortRef, CFMessagePortRef, or CFSocketRef)来创建合适的对象

自定义input source

你必须用Core Foundation中和CFRunLoopSourceRef相关的方法来创建自定义input source。你用几个回调函数来配置自定义input source。Core Foundation会在不同的时间点调用这些方法来配置source,处理事件,移出run loop时关闭source。

除了定义事件触发时自定义source的行为,你必须定义事件的传递机制。这部分的source运行在不同的线程中,负责给input source提供数据,当数据可以处理时,负责通知input source。事件的传递机制取决于你自己,但不必过分复杂。

创建自定义input source的方法,可以参考

Defining a Custom Input Source. 和 CFRunLoopSource Reference

Cocoa Perform Selector Sources

除了基于端口的source,Cocoa还定义了一种自定义input source,可以在任意线程上perform selector。和基于端口的source一样,perform selector的请求在线程里是串行的,这样就减少了多线程中同步问题的发生。和基于端口的input source不一样,perform selector在完成后,会把自己从run loop中移除。

注:OS X v10.5以前的版本,perform selector sources 更多的是用来给主线程发送消息,但是在OS X v10.5以更高版本和iOS中,你可以用它给任意线程发送消息。

当你在其他线程中performing a selector时,目标线程必须有一个激活的Run loop。这意味着对于你自己创建的线程,你必须显式地运行run loop。因为主线程会自己启动它自己的run loop,所以在程序启动时就可以在主线程perform selector。因为Run loop通过每次循环来处理所有排列的perform selector调用,而不时通过每次的循环迭代来处理perform selector。

表3-2列出来NSObject 定义的可以在其他线程perform selectors的方法,由于这些方法是在NSObject中定义的,你可以在任何使用Objective-C的线程中使用这些方法,包括POSIX线程。这些方法并不会为perform selector创建新的线程。

表3-2 在其他线程上perform selector

performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
在run loop的下一个循环中,在程序的主线程中perform selector。这些方法提供了可以阻塞当前线程的选项。

performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
在任何有NSThread对象的线程上perform selector。这些方法提供了可以阻塞当前线程的选项。

performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
在当前线程的下一个循环中延迟perform selector。由于它需要等到run loop的下一个循环周期,这些方法自动提供了一个最小延迟调用。多个selectors会放到队列中管理。

cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:
取消那些延迟调用的perform selectors。

详情请可以参考NSObject Class Reference.

定时器source

定时器source在未来的某个时刻以同步的方式传递事件到你的线程中。对线程来说,定时器是一种可以唤醒(notify)自己的方式。举个例子,在用户连续输入后的一段时间后,搜索框可以自动触发一次搜索。通过这种延迟的方式,在发起搜索前,让用户尽可能多地输入内容。

尽管这样产生了基于时间的通知,定时器不是真正意义上的实时。和Input source一样,定时器和run loop的特定的mode相关。如果一个定时器不在run loop的当前的mode中,它会等到run loop的mode切换到它支持的mdoes中时才触发。类似的,如果一个定时器在run loop处理其他回调程序的时候触发,它得等到run loop处理完。如果run loop压根就没有启动,拿定时器永远也不会触发。

你可以让你的定时器只触发一次或多次。一个重复触发的定时器会事先确定触发的时间,和真实的时间是不一样的。举个例子,如果一个定时器计划在某个特定的时间触发,并每隔5s重复触发。这个计划的触发时间总是以原始时间开始计算,以5s的间隔重复,哪怕实际的触发时间因为某种原因延迟了。如果延迟得太久了,错过了1个或多个间隔,定时器只会给错过的这段时间触发一次,接下来的计划也会重新计算。

更多信息请参考 Configuring Timer Sources. NSTimer Class ReferenceCFRunLoopTimer Reference.

Run Loop Observers

source在同步或者异步事件发生时才触发,observrs则在run loop运行期间的特定时间触发。你可能用observers来准备线程去处理特定的事件,或者在线程将要休眠前,做一些准备工作。你可以在run loop的以下几种情况用observers来关联。

1、开始进入run loop
2、当run loop将要开始处理定时器时。
3、当run loop将要处理input sources时。
4、当run loop将要休眠时。
5、当run loop以唤醒,但是在处理唤醒他的事件之前。
6、当推出run loop时。

你可以通过Core Foundation给apps添加observers。你可以用 CFRunLoopObserverRef类型的对象来创建一个observer。这个对象会跟踪感兴趣的自定义回调方法和活动。

和定时器类似,observers可以使用一次或多次。只使用一次的observer会在使用过后,自己移除,重复使用的observers则会一直保留着。你创建的时候,可以指明是一次还是多次。

更多信息请参考 Configuring the Run Loop. 和 CFRunLoopObserver Reference.