什么是RunLoop

RunLoop就是一个永不停止的循环,在这个循环中,系统将一直进行“等待-接收消息-处理-等待”的循环。iOS中的RunLoop保证了系统可以一直运行,而不是执行完成当前任务后就退出。在iOS的主线程中自动运行着一个RunLoop,即main.m文件中的main函数返回的UIApplicationMain对象自动包含的。

其主要起如下作用:

  1. 等待用户的输入和发出的事件;
  2. 决定事件的处理顺序;
  3. 事件发送可以同时进行;
  4. 在等待期间几乎不需要占用CPU资源。

RunLoop的实现

OC中的Core Foundation库中有一层对RunLoop的实现:CFRunLoopRef,主要包含RunLoop的结构和一系列底层的C语言API,而后在Foundation库中对CFRunLoopRef有进一步的封装:NSRunLoop。

RunLoop结构

CFRunLoopRef

img

CFRunLoopModeRef

对于任意一个CFRunLoopModeRef对象,其中包含三部分,分别是CFRunLoopSourceRef的Set,CFRunLoopObserverRef的Array和CFRunLoopTimerRef的Array。如果一个mode中这三个对象均为空,则不会进入这个RunLoop,而同一对象反复加入mode也只会起效一次。因此,如果要切换mode,就必须先退出当前mode,然后再进入新的mode,从而做到mode与mode之间的分离。

要往mode上添加或移除对象,可以使用以下函数:

1
2
3
4
5
6
CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
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
2
3
4
5
6
7
8
9
10
// CFRunModeRef
CFRunLoopAddTimer(CFRunLoopGetCurrent(), CFRunLoopTimerCreate(kCFAllocatorDefault, 0<开始时间>, 1.f<间隔时间>, 0<flag,默认为0即可>, 1<优先级>, func<回调函数>, NULL<传入的参数,info>), kCFRunLoopCommonModes);
void func(CFRunLoopTimerRef timer, void *info){
// 具体执行代码
}
// NSRunLoop
NSTimer *timer = [NSTimer timerWithTimeInterval:1.f repeats:YES block:^(NSTimer * _Nonnull timer) {
// 具体执行代码
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:**NSRunLoopCommonModes**];

CFRunLoopTimerRef

NSTimer是对这个对象的封装

CFRunLoopSourceRef

  • source0:处理如UIEvent,CFSocket这样的事件

  • source1:Mach port驱动,CFMachport,CFMessagePort

CFRunLoopObserverRef

Cocoa框架中很多机制比如CAAnimation等都是由RunLoopObserver触发的,observer到当前状态的变化进行可以通知,通知的时机有:

1
2
3
4
5
6
7
8
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
};

RunLoop的内部逻辑

runlooplogic

RunLoop的应用

AutoReleasePool

App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()。

AutoReleasePool

第一个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到主线程,则说明未发生卡顿,否则则说明有卡顿。(注意其实线程切换有损耗)

slowdown1

此外可以使用RunLoop实现

slowdown2

slowdown3

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
2
3
4
5
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 具体操作
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];// 如果没有信息要在线程间传递的话可以不用这个port
[[NSRunLoop currentRunLoop] run];
});

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方法,完成一次完整的响应。

stack1

eventhandler.PNG

stack2

手势操作

手势操作则是在上面事件响应的基础上进行的,系统默认会注册一个回调函数为_UIGestureRecognizerUpdateObserver的observer

gesture

当上面的 handleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。

当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,_UIGestureRecognizerUpdateObserver这个回调都会进行相应处理。

UI更新

系统会注册一个回调函数为_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv的observer用于处理UI上的更新,监听BeforeWaiting和Exit事件。

uiupdate.PNG

每次操作UI时,系统都会将这个UIView或者CALayer提交到一个全局的容器里面,在下一个runloop的时候统一调用上面那个回调函数执行ui的更新。(高亮处即为上面的那个回调函数)

stack3

参考

https://blog.ibireme.com/2015/05/18/runloop/

https://www.jianshu.com/p/417591dcd2db

https://github.com/ming1016/study/wiki/CFRunLoop