什么是RunLoop
RunLoop就是一个永不停止的循环,在这个循环中,系统将一直进行“等待-接收消息-处理-等待”的循环。iOS中的RunLoop保证了系统可以一直运行,而不是执行完成当前任务后就退出。在iOS的主线程中自动运行着一个RunLoop,即main.m文件中的main函数返回的UIApplicationMain对象自动包含的。
其主要起如下作用:
- 等待用户的输入和发出的事件;
- 决定事件的处理顺序;
- 事件发送可以同时进行;
- 在等待期间几乎不需要占用CPU资源。
RunLoop的实现
OC中的Core Foundation库中有一层对RunLoop的实现:CFRunLoopRef,主要包含RunLoop的结构和一系列底层的C语言API,而后在Foundation库中对CFRunLoopRef有进一步的封装:NSRunLoop。
RunLoop结构
CFRunLoopRef
CFRunLoopModeRef
对于任意一个CFRunLoopModeRef对象,其中包含三部分,分别是CFRunLoopSourceRef的Set,CFRunLoopObserverRef的Array和CFRunLoopTimerRef的Array。如果一个mode中这三个对象均为空,则不会进入这个RunLoop,而同一对象反复加入mode也只会起效一次。因此,如果要切换mode,就必须先退出当前mode,然后再进入新的mode,从而做到mode与mode之间的分离。
要往mode上添加或移除对象,可以使用以下函数:
1 | CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName); |
CommonMode的使用
其中注意CommonModes这个概念,默认1和2这两个mode都具有common属性,设置mode的common属性可以使用以下函数,会将这个mode存进_commonModes数组中:
1 | CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName); |
具有common属性的mode在发生变化的时候,会将_commonModeItems中的所有对象自动的添加到所有具有common属性的mode中。将对象添加到_commonModeItems需要将对象绑定或添加到kCFRunLoopCommonModes这个mode上。举例如下:
主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个 Mode 都已经被标记为”Common”属性。DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。当你创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,并且也不会影响到滑动操作。
有时你需要一个 Timer,在两个 Mode 中都能得到回调,一种办法就是将这个 Timer 分别加入这两个 Mode。还有一种方式,就是将 Timer 加入到顶层的 RunLoop 的 “commonModeItems” 中。”commonModeItems” 被 RunLoop 自动更新到所有具有”Common”属性的 Mode 里去。
具体操作如下:
1 | // CFRunModeRef |
CFRunLoopTimerRef
NSTimer是对这个对象的封装
CFRunLoopSourceRef
-
source0:处理如UIEvent,CFSocket这样的事件
-
source1:Mach port驱动,CFMachport,CFMessagePort
CFRunLoopObserverRef
Cocoa框架中很多机制比如CAAnimation等都是由RunLoopObserver触发的,observer到当前状态的变化进行可以通知,通知的时机有:
1 | typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { |
RunLoop的内部逻辑
RunLoop的应用
AutoReleasePool
App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()。
第一个observer监视的是Entry,其优先级最高(数字最小),回调函数会创建autoreleasepool
第二个observer监视的是BeforeWaiting(即将进入休眠)和Exit,BeforeWaiting时候会触发释放旧的自动回收池并建立新的,Exit的时候只会释放旧的pool,其优先级最低,保证其在其他所有事件之后调用。
NSTimer
NSTimer和CFRunLoopTimerRef是Toll-Free Bridged(无缝桥接,可以直接使用强转,具体可见Toll-Free Bridging)。为了节省资源,RunLoop并不会十分精确的调用Timer的回调函数。Timer有一个属性tolerance,表示能容忍的最大误差(尽管tolerance可以置0,但是实际上却不一定管用),而在当前周期无法执行的任务会跳过,而不会延后执行。
延迟检测
在一个异步线程上用NSTimer定时向主线程发送请求,如果在一定的时间(规定的卡顿时间)内可以dispatch到主线程,则说明未发生卡顿,否则则说明有卡顿。(注意其实线程切换有损耗)
此外可以使用RunLoop实现
GCD
RunLoop底层会用到GCD来实现,如使用dispatch_queue_t来管理mode,而GCD中的函数dispatch_async在第一个参数为主线程(即回归到主线程执行)的时候,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。
线程常驻
1 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ |
performSelector的afterDelay和onThread
当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。
事件响应
苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。
当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。这个过程的详细情况可以参考这里。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 __handleEventQueue() 进行应用内部的分发。
__handleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touche事件都是在这个回调中完成的。
通过添加__IOHIDEventSystemClientQueueCallback和__handleEventQueue这两个方法的Symbolic Breakpoint,观察调用栈可以知道,在点击UIButton触发点击事件的时候,首先会调用基于source1的__IOHIDEventSystemClientQueueCallback方法,然后才会调用基于source0的__handleEventQueue方法,完成一次完整的响应。
手势操作
手势操作则是在上面事件响应的基础上进行的,系统默认会注册一个回调函数为_UIGestureRecognizerUpdateObserver的observer
当上面的 handleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。
当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,_UIGestureRecognizerUpdateObserver这个回调都会进行相应处理。
UI更新
系统会注册一个回调函数为_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv的observer用于处理UI上的更新,监听BeforeWaiting和Exit事件。
每次操作UI时,系统都会将这个UIView或者CALayer提交到一个全局的容器里面,在下一个runloop的时候统一调用上面那个回调函数执行ui的更新。(高亮处即为上面的那个回调函数)
参考
https://blog.ibireme.com/2015/05/18/runloop/