一、Runloop 简介
Runloop 是通过内部维护事件循环来对事件/消息进行管理的一个对象。
事件循环(状态切换)
- 没有消息需要处理时,休眠以避免资源占用(用户态 -> 内核态)
- 有消息需要处理时,立刻被唤醒(内核态 -> 用户态)
事件/消息管理:Runloop 通过 mach_msg() 函数接收、发送消息来进行管理。
二、Runloop 数据结构
NSRunloop 是 CFRunloop 的封装,提供了面向对象的 API。
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
// ...
CFStringRef _name;
// ...
CFMutableSetRef _sources0; // <set>
CFMutableSetRef _sources1; // <set>
CFMutableArrayRef _observers; // <Array>
CFMutableArrayRef _times; // <Array>
// ...
};
// CFRunloop.h 类型重命名
typedef struct __CFRunLoop *CFRunLoopRef;
// CFRunloop.c 结构体
struct __CFRunLoop {
// ...
pthread_t _pthread; // runloop 执行线程(runloop 和线程的关系是一一对应)
// ...
CFMutableSetRef _commonModes; // <set> String UITrackingRunloopMode / KCFRunloopDefaultMode (一个存储了被标记为 common modes 的模式集合)
CFMutableSetRef _commonModeItems; // <set> Timer / Observer / Source
CFRunLoopModeRef _currentMode; // 当前运行的 mode
CFMutableSetRef _modes; // 内置的 modes
// ...
};
Runloop 内部存在一个 modes 集合,但 Runloop 只能运行一个 Mode, Runloop 只会处理它当前 Mode 的事件。
Runloop 运行模式
- kCFRunLoopDefaultMode, App的默认运行模式,通常主线程是在这个运行模式下运行
- UITrackingRunLoopMode, 跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响)
- kCFRunLoopCommonModes, 伪模式,不是一种真正的运行模式
- UIInitializationRunLoopMode:在刚启动App时第进入的第一个Mode,启动完成后就不再使用
- GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到
Runloop 中的 commonModes 是一个集合,里面存储着被标记 kCFRunloopCommonModes 的 mode 的 name。
CFRunloopMode 中的 Item
Source0:只包含一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source), 将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop)来唤醒 Runloop,让其处理这个事件。
Source1:基于mach_port,来自系统内核或者其他进程或线程的事件,可以主动唤醒休眠中的 Runloop。
Observer – CFRunloopObserver
- KCFRunloopEntry (runloop 准备启动)
- KCFRunloopBeforeTimers (通知观察者,runloop 将要对 Timer 的一些相关事件进行处理了)
- KCFRunloopBeforeSoureces(将要处理一些 Sources 事件)
- KCFRunloopBeforeWaiting(即将要发生用户态到内核态的切换)
- KCFRunloopAfterWaiting(内核态转换为用户态)
- KCFRunloopExit(runloop 退出通知)
Timer – CFRunloopTimer:是基于时间的触发器,当加入到 Runloop 时,Runloop 会注册对应的时间点,当时间点到时,Runloop 会被唤醒以执行回调。
Runloop 中的 commonModeItems 是当前运行在 commonModes 模式下的 CFRunloopSource / CFRunloopObserver / CFRunloopTimer,其实就是 kCFRunLoopDefaultMode 和 UITrackingRunLoopMode 的 source1 / timter / observers 都包含在 commonModeItems 里面。
当 commonModes 里每多一个 mode, commonModeItems 里就会多一组 source1 / timter / observers
三、Runloop 执行原理
Runloop 伪代码:
SetupThisRunLoopRunTimeoutTimer(); // by GCD timer
//通知即将进入runloop__CFRUNLLOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(KCFRunLoopEntry);
do {
__CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
__CFRunLoopDoObservers(kCFRunLoopBeforeSources);
__CFRunLoopDoBlocks(); //一个循环中会调用两次,确保非延迟的NSObject PerformSelector调用和非延迟的dispatch_after调用在当前runloop执行。还有回调block
__CFRunLoopDoSource0(); //例如UIKit处理的UIEvent事件
CheckIfExistMessagesInMainDispatchQueue(); //GCD dispatch main queue
__CFRunLoopDoObservers(kCFRunLoopBeforeWaiting); //即将进入休眠,会重绘一次界面
// 休眠,等待事件唤醒
var wakeUpPort = SleepAndWaitForWakingUpPorts();
// mach_msg_trap,陷入内核等待匹配的内核mach_msg事件
// Zzz...
// Received mach_msg, wake up
__CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
// Handle msgs
if (wakeUpPort == timerPort) {
__CFRunLoopDoTimers();
} else if (wakeUpPort == mainDispatchQueuePort) {
//GCD当调用dispatch_async(dispatch_get_main_queue(),block)时,libDispatch会向主线程的runloop发送mach_msg消息唤醒runloop,并在这里执行。这里仅限于执行dispatch到主线程的任务,dispatch到其他线程的仍然是libDispatch来处理。
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
} else {
__CFRunLoopDoSource1(); //CADisplayLink是source1的mach_msg触发?
}
__CFRunLoopDoBlocks();
} while (!stop && !timeout);
//通知observers,即将退出runloop
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBERVER_CALLBACK_FUNCTION__(CFRunLoopExit);
3.1 runloop 处理的 6 类事件
1、timer (延迟的 PerformSelector 方法调用,延迟的 dispatch_after,timer事件)
2、observer(runloop中状态变化时进行通知(UI 观察者等))
a. 卡顿检测(1、开辟新线程定时去获取值的状态;2、observer 监听 runloop 的 beforeTimer 开始, beforeWaitingSleep / exit 结束)
3、source0(app 内部事件处理:UIEvent 时间,CFSocket 事件,触摸事件其实是Source1接收系统事件后在回调 __IOHIDEventSystemClientQueueCallback() 内触发的 Source0,Source0 再触发的 _UIApplicationHandleEventQueue()。source0一定是要唤醒runloop及时响应并执行的,如果runloop此时在休眠等待系统的 mach_msg事件,那么就会通过source1来唤醒runloop执行。)
4、source1(处理系统内核的mach_msg事件)
5、Block(PerformSelector 非延迟方法、dispatch_after 立刻调用,block 调用)
6、Main Queue(GCD 中 dispatch 到 main queue 的 block 会被 dispatch 到 main loop执行)
3.2 Runloop mode 切换
上节我们知道,Runloop 每次只能在当前 mode 下执行,那如果需要在不同的 mode 下切换系统是怎么处理的呢?比如界面从静止到滑动时,mode 是如何由 NSRunloopDefaultMode 切换到 UITrackingRunloopMode呢。
CFRunloopRunSpecific 是启动 Runloop 和指定 Runloop 在那个 mode 下执行的 mode, 这个函数一般式操作系统进行 mode 的切换。
CFRunloopRunSpecific 会保持前一次 mode 的状态属性(stopped 和 currentmode)然后发出即将要进入新 mode 通知,然抽进入 __CFRunloopRun(__CFRunloopRun 会创建一个循环),然后这个 mode 运行结束后再发已退出 mode 通知,再恢复前一次的 stopped 和 currentmode。
四、Runloop 实际应用
4.1 AutoreleasePool
App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。
第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
第二个 Observer 监视了两个事件:
- BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;
- Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。
4.2 事件响应
苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。
- 当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。
- SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。
- _UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。
4.3 手势识别
当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。
苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。
当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。
4.4 界面更新
runloop 在 APP 启动的时候会注册一个 UI Observer, UI 变动(frame 变动、UIView/CALayer 层级的变动、 手动设置了 setNeedsLayout/setNeedsDisplay 等)的操作将会提交到全局容器,当 UI Observer 监听到主线程 runloop 进入休眠或者退出的状态,回调去执行一个很长的函数:_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。
4.5 按钮点击
首先是由那个Source1 接收IOHIDEvent,之后在回调 __IOHIDEventSystemClientQueueCallback() 内触发的 Source0,Source0 再触发的 _UIApplicationHandleEventQueue()。所以UIButton事件看到是在 Source0 内的。