RunLoop是一种事件处理循环机制,类似于中断处理,它可以监听一个或多个定时器源(Timer Sources)和输入源(Input Sources),当没有事件时,它让线程休眠;当有事件发生时,系统唤醒线程,把事件放入RunLoop队列,RunLoop再分发给用户指定的事件处理入口函数。由此可以看出,RunLoop是为了低功耗
而设计的,它不会浪费CPU的时间,不会阻止CPU进入低功耗模式,对于对功耗敏感的移动终端来说,在恰当的场合使用它能让你的程序获得更好的性能。
我们可以通过Fondation框架中封装的NSRunLoop
和CoreFoundation框架CFRunLoop.h
中定义的相关方法来使用RunLoop,其中NSRunLoop是对CFRunLoop.h的OC封装,我们先从NSRunLoop开始学习怎么使用RunLoop以及其中的一些细节。
上图中是由runUtilDate:启动的RunLoop的结构,主要由以下特点:
RunLoop是和线程紧密相连的,创建一个线程的同时系统就已经创建好了一个RunLoop。主线程的RunLoop是随着主线程自动启动的,其他子线程的RunLoop要手动启动。
RunLoop是在某一模式上运行的,可以使用
- (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的事件处理做的区分,上图中的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的类方法:
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;
会把一个Timer Source添加到当前RunLoop的NSDefaultRunLoopMode
模式上,当触摸事件发生时,主线程被设置为UITrackingRunLoopMode
模式,Timer 事件不会得到触发,可以通过调用
- (void)addTimer:(NSTimer *)timer forMode:(NSString *)mode;
将mode设置为NSRunLoopCommonModes来解决;如果想让Timer只在触摸屏幕的时候才被触发,可以将mode设置为UITrackingRunLoopMode。
用于接受系统事件,属于内部的Run Loop模式。
Custom mdoe
可以设置自定义模式,也可以通过
CF_EXPORT void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef mode);
将Custom mode添加到kCFRunLoopCommonModes中去。
事件源分为两大类,一是输入源(Input Sources
),一是定时器源(Timer Sources
)
输入源按按照事件来源的不同可分为Port-Based Sources
、Custom Input Sources
和Cocoa 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.
Foundation框架中,可以使用NSPort
及其子类 : NSMachPort
、NSMessagePort
和NSSocketPort
.NSMachPort可作为线程之间的通讯通道。例如在主线程创建子线程时传入一个NSPort对象,这样主线程就可以和这个子线程通讯啦,如果要实现双向通讯,那么子线程也需要回传给主线程一个NSPort。
CF框架中,相应的有CFMachPortRef
、CFMessagePortRef
和CFSocketRef
,使用起来会比Foundation稍微复杂一点。
Input Sources如果按照事件的触发过程可以分为Version0
和Version1
。他们对应于不同的上下文结构体:CFRunLoopSourceContext
和CFRunLoopSourceContext1
中的Version字段。
Version0的输入源是受程序控制的,如果一个消息到达了输入源,你必须在程序中使用CFRunLoopSourceSignal
来将这个输入源标记为待触发
,然后再唤醒RunLoop。比如Socket Port就属于此类型。 Version1的输入源是受内核控制的,当有消息到达Mach Port时,系统会自动将这个输入源标记为待触发
,并自动唤醒RunLoop。Mach Port和Message Port即属于此种类型。
只能使用CF框架中的API,可以创建上面所说的Version0 Version1两种类型的输入源。
相关的API簇如下如下所示:
//可以使用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:
它的特点如下:
- 可以运行在任意线程上
- 和port-based sources类似,selector连续的添加到目标线程上
- 和它不同到是,selecor执行完之后,就把自己从run loop中移除。(实际测试只有
performSelector:withObject:afterDelay:
才会被移除???)- 如果在一次run loop循环中有多个selector同时等待被触发,那么可以一次性触发所有这些selector。
定时器事件作为Timer Sources添加到RunLoop中。如果一个搜索控件想在用户输入完成等待一段事件后自动开始一次搜索,就可以使用定时器来完成。
定时器源也是要在RunLoop运行在其指定的模式上才能被触发,并且可以指定被触发一次还是可以被重复触发,作为主线程的定时器如果不想被主线程延迟,应该添加到NSRunLoopCommonModes上。比如一个5s重复一次的定时器被延迟了,在下一次RunLoop中被触发之后,还是继续以5s重复一次的频率运行。
再次需要注意的是:定时器被触发之后,会被移除RunLoop,不会中断当前RunLoop,即使使用
SInt32 CFRunLoopRunInMode (
CFStringRef mode,
CFTimeInterval seconds,
Boolean returnAfterSourceHandled
);
并且returnAfterSourceHandled=YES,也不会使这个函数返回。
//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:
//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
官方文档的建议是:
hrchen提到以下应用场景:
使用子线程去执行周期性任务 NSURLConnection在子线程中发起异步请求
iOS多线程编程Part 1/3 - NSThread & Run Loop
我在github上fork了一一份hrchen的代码。
代码下载