NSOiOiOS多线程编程(2/4):RunLoop 2013-12-22
RunLoop是一种事件处理循环机制,类似于中断处理,它可以监听一个或多个定时器源(Timer Sources)和输入源(Input Sources),当没有事件时,它让线程休眠;当有事件发生时,系统唤醒线程,把事件放入RunLoop队列,RunLoop再分发给用户指定的事件处理入口函数。由此可以看出,RunLoop是为了低功耗
而设计的,它不会浪费CPU的时间,不会阻止CPU进入低功耗模式,对于对功耗敏感的移动终端来说,在恰当的场合使用它能让你的程序获得更好的性能。
RunLoop
我们可以通过Fondation框架中封装的NSRunLoop
和CoreFoundation框架CFRunLoop.h
中定义的相关方法来使用RunLoop,其中NSRunLoop是对CFRunLoop.h的OC封装,我们先从NSRunLoop开始学习怎么使用RunLoop以及其中的一些细节。
上图中是由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 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.
Port-Based Sources
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即属于此种类型。Custom Input Sources
只能使用CF框架中的API,可以创建上面所说的Version0 Version1两种类型的输入源。
Cocoa Perform Selector Sources
相关的API簇如下如下所示:
1 | //可以使用wait=YES来进行阻塞执行 |
它的特点如下:
- 可以运行在任意线程上
- 和port-based sources类似,selector连续的添加到目标线程上
- 和它不同到是,selecor执行完之后,就把自己从run loop中移除。(实际测试只有
performSelector:withObject:afterDelay:
才会被移除???)- 如果在一次run loop循环中有多个selector同时等待被触发,那么可以一次性触发所有这些selector。
2.Tiemr Sources
定时器事件作为Timer Sources添加到RunLoop中。如果一个搜索控件想在用户输入完成等待一段事件后自动开始一次搜索,就可以使用定时器来完成。
定时器源也是要在RunLoop运行在其指定的模式上才能被触发,并且可以指定被触发一次还是可以被重复触发,作为主线程的定时器如果不想被主线程延迟,应该添加到NSRunLoopCommonModes上。比如一个5s重复一次的定时器被延迟了,在下一次RunLoop中被触发之后,还是继续以5s重复一次的频率运行。
再次需要注意的是:定时器被触发之后,会被移除RunLoop,不会中断当前RunLoop,即使使用
1 | SInt32 CFRunLoopRunInMode ( |
并且returnAfterSourceHandled=YES,也不会使这个函数返回。
NSRunLoop 接口
1 | //Accessing Run Loops and Modes |
CFRunLoop 接口
1 | //Getting a Run Loop |
什么时候使用RunLoop
官方文档的建议是:
- 需要使用Port或者自定义Input Source与其他线程进行通讯。
- 需要在线程中使用Timer。
- 需要在线程上使用performSelector*方法。
- 需要让线程执行周期性的工作。
hrchen提到以下应用场景:
- 使用自定义Input Source和其他线程通信
- 子线程中使用了定时器
- 使用任何performSelector*到子线程中运行方法
使用子线程去执行周期性任务
NSURLConnection在子线程中发起异步请求参考资料
- Threading Programming Guide
- NSRunLoop Class Reference
- CFRunLoop Reference
- CFRunLoopObserver Reference
- CFRunLoopSource Reference
- iOS多线程编程指南
- objccn.io #2 并发编程
Sample Code
我在github上fork了一一份hrchen的代码。
代码下载