RunLoop是一种事件处理循环机制,类似于中断处理,它可以监听一个或多个定时器源(Timer Sources)和输入源(Input Sources),当没有事件时,它让线程休眠;当有事件发生时,系统唤醒线程,把事件放入RunLoop队列,RunLoop再分发给用户指定的事件处理入口函数。由此可以看出,RunLoop是为了低功耗而设计的,它不会浪费CPU的时间,不会阻止CPU进入低功耗模式,对于对功耗敏感的移动终端来说,在恰当的场合使用它能让你的程序获得更好的性能。

RunLoop

我们可以通过Fondation框架中封装的NSRunLoop和CoreFoundation框架CFRunLoop.h中定义的相关方法来使用RunLoop,其中NSRunLoop是对CFRunLoop.h的OC封装,我们先从NSRunLoop开始学习怎么使用RunLoop以及其中的一些细节。

imag



上图中是由runUtilDate:启动的RunLoop的结构,主要由以下特点:

  • RunLoop是和线程紧密相连的,创建一个线程的同时系统就已经创建好了一个RunLoop。主线程的RunLoop是随着主线程自动启动的,其他子线程的RunLoop要手动启动。

  • RunLoop是在某一模式上运行的,可以使用

1
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate

来指定RunLoop运行在哪种模式上;事件源在被添加到RunLoop时,需要指定在何种模式下被触发,RunLoop观察者也要指定模式,用于指定观察何种模式下的RunLoop生命周期。

  • RunLoop只处理两大类事件:Input Sources 和 Timer Sources,对Input sources中的三种异步事件基于端口的自定义的performSelector:onThread簇。Timer Sources中的同步事件。需要注意的是performSelector:withObject:afterDelay:执行完会被移除RunLoop,而performSelector:onThread:withObject:waitUntilDone:在wait=NO时,则不会,而wait=YES时,会立即执行,不会添加到RunLoop中。

  • Timer Sources事件触发后,会被移出RunLoop队列,本次RunLoop不会退出;Input Sources事件被触发后,不会被移出RunLoop队列,本次RunLoop结束。如果RunLoop中只有一个Input Sources,如果不显式的移除,则永远存在;如果RunLoop中只有一个Timer Sources,如果Timer的repeat=NO,或者Timer被终止,则RunLoop则会因没有事件源而立刻退出。

  • 如果RunLoop中没有事件源,则立刻退出。

RunLoop的处理事件时序

  • 1) 进入Run Loop运行,此时会通知观察者进入Run Loop;
  • 2) 如果有Timer即将触发时,通知观察者;
  • 3) 如果有非Port的Input Sourc即将e触发时,通知观察者;
  • 4)触发非Port的Input Source事件源;
  • 5)如果基于Port的Input Source事件源即将触发时,立即处理该事件,跳转到步骤9;
  • 6)通知观察者当前线程将进入休眠状态;
  • 7)将线程进入休眠状态直到有以下事件发生:基于Port的Input Source被触发、Timer被触发、Run Loop运行时间到了过期时间、Run Loop被唤醒。
  • 8) 通知观察者线程将要被唤醒。
  • 9) 处理被触发的事件:
    • 如果是用户自定义的Timer,处理Timer事件后重新启动Run Loop进入步骤2;
    • 如果是其他Input Source事件源有事件发生,直接处理这个事件;
    • 如果线程被唤醒又没有到过期时间,则进入步骤2;
  • 10)到达此步骤说明Run Loop运行时间到期,或者是非Timer的Input Source事件被处理后,Run Loop将要退出,退出前通知观察者线程已退出。

RunLoop的运行模式mode

运行模式就是根据一个用字符串标记的名字来对RunLoop的事件处理做的区分,上图中的Input Sources和Timer Sources,以及后面要提到的RunLoop观察者都是绑定到指定的模式上(也可以选择绑定到所有模式上),只有RunLoop在此模式上运行时,相应的事件才会被触发,相应的观察者才会收到消息。

iOS下RunLoop的运行模式主要有:

  • NSDefaultRunLoopMode - kCFRunLoopDefaultMode

    线程的默认运行模式,大部分情况下都是运行在此种模式下的。上面两个分别对应Foundation和CF框架中的不同名字,如果打印出来:NSLog(@"%@",NSDefaultRunLoopMode),值就是kCFRunLoopDefaultMode。
    系统有时会改变RunLoop的运行模式,也可以通过- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate来指定运行模式。

  • NSRunLoopCommonModes - kCFRunLoopCommonModes

    NSRunLoopCommonModes代表一个modes集合,是为了方便事件源和RunLoop观察者绑定而设定的。添加到NSRunLoopCommonModes中的事件源或观察者就等于添加到了集合中所有的模式之上。通过CFRunLoopAddCommonMode可以向NSRunLoopCommonModes集合中添加自定义mode。注意,即使在代码中先把Timer添加到了modes集合中,又向集合中添加了新的modeA,如果线程运行于modeA,那么Timer事件一样会被触发,即与次序无关。

  • UITrackingRunLoopMode:

    用于跟踪触摸事件触发的模式(例如UIScrollView上下滚动),主线程当触摸事件触发时会设置为这个模式,可以用来在控件事件触发过程中设置Timer。比如,在主线程中通过调用NSTimer的类方法:

1
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;

会把一个Timer Source添加到当前RunLoop的NSDefaultRunLoopMode模式上,当触摸事件发生时,主线程被设置为UITrackingRunLoopMode模式,Timer 事件不会得到触发,可以通过调用

1
- (void)addTimer:(NSTimer *)timer forMode:(NSString *)mode;

将mode设置为NSRunLoopCommonModes来解决;如果想让Timer只在触摸屏幕的时候才被触发,可以将mode设置为UITrackingRunLoopMode。

  • GSEventReceiveRunLoopMode

    用于接受系统事件,属于内部的Run Loop模式。

  • Custom mdoe

    可以设置自定义模式,也可以通过

1
CF_EXPORT void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef mode);

将Custom mode添加到kCFRunLoopCommonModes中去。

RunLoop的事件源

事件源分为两大类,一是输入源(Input Sources),一是定时器源(Timer Sources

1.输入源(Input Sources)

输入源按按照事件来源的不同可分为Port-Based SourcesCustom Input SourcesCocoa Perform Selector Sources
输入源可以添加到多个RunLoop或多个运行模式上,这样这个输入源就能够得到即使的处理,比如通过网口接收到数据后想进行数据解析,解析方法就能得到最快的响应。官方文档描述如下:

A run loop source can be registered in multiple run loops and run loop modes at the same time. When the source is signaled, whichever run loop that happens to detect the signal first will fire the source. Adding a source to multiple threads’ run loops can be used to manage a pool of “worker” threads that is processing discrete sets of data, such as client-server messages over a network or entries in a job queue filled by a “manager” thread. As messages arrive or jobs get added to the queue, the source gets signaled and a random thread receives and processes the request.

  • Port-Based Sources

    Foundation框架中,可以使用NSPort及其子类 : NSMachPortNSMessagePortNSSocketPort .NSMachPort可作为线程之间的通讯通道。例如在主线程创建子线程时传入一个NSPort对象,这样主线程就可以和这个子线程通讯啦,如果要实现双向通讯,那么子线程也需要回传给主线程一个NSPort。
    CF框架中,相应的有CFMachPortRefCFMessagePortRefCFSocketRef,使用起来会比Foundation稍微复杂一点。

    Input Sources如果按照事件的触发过程可以分为Version0Version1。他们对应于不同的上下文结构体:CFRunLoopSourceContextCFRunLoopSourceContext1中的Version字段。

    Version0的输入源是受程序控制的,如果一个消息到达了输入源,你必须在程序中使用CFRunLoopSourceSignal来将这个输入源标记为待触发,然后再唤醒RunLoop。比如Socket Port就属于此类型。
    Version1的输入源是受内核控制的,当有消息到达Mach Port时,系统会自动将这个输入源标记为待触发,并自动唤醒RunLoop。Mach Port和Message Port即属于此种类型。

  • Custom Input Sources

    只能使用CF框架中的API,可以创建上面所说的Version0 Version1两种类型的输入源。

  • Cocoa Perform Selector Sources

    相关的API簇如下如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//可以使用wait=YES来进行阻塞执行
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:

//同样可以使用wait=YES来进行阻塞执行,通过这种方式提交到run loop,
//在执行完之后不会被移除run loop,和官方文档矛盾?
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:

//通过这种方式提交到run loop中的selecto可以通过最后一组API取消
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:

cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:

它的特点如下:

  1. 可以运行在任意线程上
  2. 和port-based sources类似,selector连续的添加到目标线程上
  3. 和它不同到是,selecor执行完之后,就把自己从run loop中移除。(实际测试只有performSelector:withObject:afterDelay:才会被移除???)
  4. 如果在一次run loop循环中有多个selector同时等待被触发,那么可以一次性触发所有这些selector。

2.Tiemr Sources

定时器事件作为Timer Sources添加到RunLoop中。如果一个搜索控件想在用户输入完成等待一段事件后自动开始一次搜索,就可以使用定时器来完成。

定时器源也是要在RunLoop运行在其指定的模式上才能被触发,并且可以指定被触发一次还是可以被重复触发,作为主线程的定时器如果不想被主线程延迟,应该添加到NSRunLoopCommonModes上。比如一个5s重复一次的定时器被延迟了,在下一次RunLoop中被触发之后,还是继续以5s重复一次的频率运行。
再次需要注意的是:定时器被触发之后,会被移除RunLoop,不会中断当前RunLoop,即使使用

1
2
3
4
5
SInt32 CFRunLoopRunInMode (
CFStringRef mode,
CFTimeInterval seconds,
Boolean returnAfterSourceHandled
);

并且returnAfterSourceHandled=YES,也不会使这个函数返回。

NSRunLoop 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//Accessing Run Loops and Modes
+ currentRunLoop
– currentMode
– limitDateForMode:
+ mainRunLoop
– getCFRunLoop

//Managing Timers
– addTimer:forMode:

//Managing Ports
– addPort:forMode:
– removePort:forMode:

//Running a Loop
/*
@brief: 如果有事件源则以默认模式重复调用– runMode:beforeDate: ,函数不返回;
如果没有事件源返回。
*/

– run

/*
@brief:以指定模式运行完一次,或超时,即退出本次run loop,函数返回。
@parameter:mode 运行模式 data:超时时间
@return YES 如果run loop能够启动,在处理完一次输入源,超时时间到
NO 如果run loop没能够启动,如没有事件源或mode参数“非法”
*/

– runMode:beforeDate:

/*
@brief: 如果有事件源则以默认模式重复调用– runMode:beforeDate: ,直至超时,函数返回
如果没有事件源则返回
*/

– runUntilDate:

/*
@brief:功能和– runMode:beforeDate:差不多,区别也大:
1.不能触发Timer,因为Timer不是Input Sources
2.没有返回值
*/

– acceptInputForMode:beforeDate:

//Scheduling and Canceling Messages
– performSelector:target:argument:order:modes:
– cancelPerformSelector:target:argument:
– cancelPerformSelectorsWithTarget:

CFRunLoop 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//Getting a Run Loop
CFRunLoopGetCurrent
CFRunLoopGetMain

//Starting and Stopping a Run Loop
CFRunLoopRun
CFRunLoopRunInMode
CFRunLoopWakeUp
CFRunLoopStop
CFRunLoopIsWaiting

//Managing Sources
CFRunLoopAddSource
CFRunLoopContainsSource
CFRunLoopRemoveSource

//Managing Observers
CFRunLoopAddObserver
CFRunLoopContainsObserver
CFRunLoopRemoveObserver

//Managing Run Loop Modes
CFRunLoopAddCommonMode
CFRunLoopCopyAllModes
CFRunLoopCopyCurrentMode

//Managing Timers
CFRunLoopAddTimer
CFRunLoopGetNextTimerFireDate
CFRunLoopRemoveTimer
CFRunLoopContainsTimer

//Scheduling Blocks
CFRunLoopPerformBlock

//Getting the CFRunLoop Type ID
CFRunLoopGetTypeID

什么时候使用RunLoop

官方文档的建议是:

  • 需要使用Port或者自定义Input Source与其他线程进行通讯。
  • 需要在线程中使用Timer。
  • 需要在线程上使用performSelector*方法。
  • 需要让线程执行周期性的工作。

hrchen提到以下应用场景:

Sample Code

我在github上fork了一一份hrchen的代码。
代码下载