RunLoop
RunLoop概念
RunLoop理解为运行循环。其本质就是一个do-while
,这里的do-while和普通的do-while循环不一样,一般的 while 循环会导致 CPU 进入忙等待状态,而 Runloop 则是一种“闲”等待,当没有事件时,Runloop 会进入休眠状态,有事件发生时, Runloop 会去找对应的 Handler 处理事件。Runloop 可以让线程在需要做事的时候忙起来,不需要的话就让线程休眠。
图中展现了 Runloop 在线程中的作用:从 input source 和 timer source 接受事件,然后在线程中处理事件。
RunLoop 作用
- 保持程序持续运行。程序启动就会自动开启一个runloop在主线程
- 处理App中的各种事件
- 节省CPU资源,提高程序性能 有事情则做事,没事情则休眠
RunLoop 和线程之间的关系
- Runloop 和线程是绑定在一起的。每个线程(包括主线程)都有一个对应的 Runloop 对象。我们并不能自己创建 Runloop 对象,但是可以获取到系统提供的 Runloop 对象。
- 主线程的 Runloop 会在应用启动的时候完成启动,其他线程的 Runloop 默认并不会启动,需要我们手动启动。
底层RunLoop的存储使用的是字典结构,线程是key,对应的runloop是value。
- 主线程是默认开启RunLoop的
- 子线程是默认不开启RunLoop的
- 子线程开启RunLoop需要我们手动开启,手动开启时会先在全局的存储字典里根据传入的线程key,查看value是否存在,不存在的话就基于线程为key创建一个新的runloop并存储进字典里。
RunLoop Mode
RunLoop Mode可以理解为RunLoop的运行模式,在苹果文档里定义了有五种运行模式。
- kCFRunLoopDefaultMode, App的默认运行模式,通常主线程是在这个运行模式下运行
- UITrackingRunLoopMode, 跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响)
- kCFRunLoopCommonModes, 伪模式,不是一种真正的运行模式
- UIInitializationRunLoopMode:在刚启动App时第进入的第一个Mode,启动完成后就不再使用
- GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到
在源码里搜索__CFRunLoopMode可以看到里面的内部结构,提取出关键的成员变量。
- _name:Mode的名字
- _sources0:
- App内部事件,由App自己管理的,像UIEvent、CFSocket、以及performSelector不带afterDelay参数的方法都是source0
- source0不能主动触发,需要调用CFRunLoopWakeUp(runloop) 来唤醒 RunLoop
- _sources1:
- 由RunLoop和内核管理,Mach port驱动,如CFMachPort、CFMessagePort。
- source1包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息
- 能主动唤醒 RunLoop 的线程
- _observers:添加的观察者
- _timers:定时器,包括NSTimer、CADisplayLink以及performSelector带afterDelay参数的方法。
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name;
Boolean _stopped;
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
__CFPortSet _portSet;
}
RunLoop的本质
RunLoop底层其实就是一个结构体,通过源码搜索__CFRunLoop。
提取出关键的成员变量_pthread、_currentMode以及_modes,可以发现
- RunLoop和线程是绑定的,每个线程会有对应的RunLop,只是主线程是默认开启的,子线程不是默认开启的,需要手动开启,然后底层就会根据传入的线程创建新的RunLoop
- _currentMode:当前运行的Mode
- _modes:多个mode的集合
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread;
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
通过上面的整理可以发现
- RunLoop管理着多个mode
- 每次RunLoop运行的的时候都必须指定一个mode作为_currentMode,如果需要执行其他mode的事务,那么就要先退出当前mode,然后切换另一个mode
- mode里面管理着source0、source1、timers、observers以及port端口的事务。
引用来自掘金博主的图片🌟
RunLoop的核心方法
通过查找源码,我们可以发现三个关键的方法
//通知观察者即将进入RunLoop
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
//进入RunLoop的关键方法
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
//通知观察者即将退出RunLoop
if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
__CFRunLoopRun
通过下面的源代码解析,可以将RunLoop的处理逻辑理为
- 通知观察者,即将进入runloop
- 通知观察者,即将处理Timers
- 通知观察者,即将处理Sources
- 执行加入到runloop的blocks
- 处理Source0,处理之后可能会再次处理blocks
- 如果存在Source1,直接跳转到第9步
- 通知观察者,即将进入休眠
- 通知观察者,结束休眠
- 执行唤醒RunLoop的事件
- 处理timer事件
- 执行gcd通过异步函数提交任务到主线程的Block
- 处理Source1事件
- 被手动唤醒,不需要执行事件,单纯起到唤醒RunLoop的功能
- 执行加入到runloop的blocks
- 根据以下情况来确定是否要退出当前RunLoop
- 进入run方法时参数表明处理完事件就返回。
- 超出run方法参数中的超时时间
- 被外部调用者强制停止了
- 调用_CFRunLoopStopMode将mode停止了
- 检测还有没有待处理的sources/timer/observer
- 以上🈚五种情况均无的话 那么就跳转到第2步,继续循环
- 通知观察者,退出RunLoop
以下代码引用掘金博主的源码解析
do {
// 消息缓冲区,用户缓存内核发的消息
uint8_t msg_buffer[3 * 1024];
//取所有需要监听的port
__CFPortSet waitSet = rlm->_portSet;
//设置RunLoop为可以被唤醒状态
__CFRunLoopUnsetIgnoreWakeUps(rl);
//1.通知 Observers: RunLoop 即将处理 Timer 回调。
if (rlm->_observerMask & kCFRunLoopBeforeTimers)
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
//2。通知 Observers: RunLoop 即将触发 Source(非port) 回调
if (rlm->_observerMask & kCFRunLoopBeforeSources)
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
// 执行被加入的block
//外部通过调用CFRunLoopPerformBlock函数向当前runloop增加block。新增加的block保存咋runloop.blocks_head链表里。
//__CFRunLoopDoBlocks会遍历链表取出每一个block,如果block被指定执行的mode和当前的mode一致,则调用__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__执行
__CFRunLoopDoBlocks(rl, rlm);
//RunLoop 触发 Source0 (非port) 回调
// __CFRunLoopDoSources0函数内部会调用__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__函数
//__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__函数会调用source0的perform回调函数,即rls->context.version0.perform
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
//处理了source0后再次处理blocks
if (sourceHandledThisLoop) {
__CFRunLoopDoBlocks(rl, rlm);
}
//
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
didDispatchPortLastTime = false;
//如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
msg = (mach_msg_header_t *)msg_buffer;
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
goto handle_msg;
}
//通知oberver即将进入休眠状态
if(!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
__CFRunLoopSetSleeping(rl);
//接收waitSet端口的消息
//等待接受 mach_port 的消息。线程将进入休眠
msg = (mach_msg_header_t *)msg_buffer;
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
// 计算线程沉睡的时长
rl->_sleepTime += (poll ? 0.0 : (CFAbsoluteTimeGetCurrent() - sleepStart));
__CFPortSetRemove(dispatchPort, waitSet);
__CFRunLoopSetIgnoreWakeUps(rl);
// runloop置为唤醒状态
__CFRunLoopUnsetSleeping(rl);
// 8. 通知 Observers: RunLoop对应的线程刚被唤醒。
if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
//收到处理的消息进行处理
handle_msg:;
// 忽略端口唤醒runloop,避免在处理source1时通过其他线程或进程唤醒runloop(保证线程安全)
__CFRunLoopSetIgnoreWakeUps(rl);
if(MACH_PORT_NULL == livePort){
// livePort为null则什么也不做
}else if(livePort == rl->_wakeUpPort){
// livePort为wakeUpPort则只需要简单的唤醒runloop(rl->_wakeUpPort是专门用来唤醒runloop的)
CFRUNLOOP_WAKEUP_FOR_WAKEUP();
}else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort){
//如果是一个timerPort
// 如果一个 Timer 到时间了,触发这个Timer的回调
// __CFRunLoopDoTimers返回值代表是否处理了这个timer
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
__CFArmNextTimerInMode(rlm, rl);
}
}else if(livePort == dispatchPort){
//如果是GCD port
//处理GCD通过port提交到主线程的事件
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}else{
// 处理source1事件(触发source1的回调)
//// runloop 触发source1的回调,__CFRunLoopDoSource1内部会调用__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply)
}
/// 执行加入到Loop的block
__CFRunLoopDoBlocks(rl, rlm);
if (sourceHandledThisLoop && stopAfterHandle) {
/// 进入loop时参数说处理完事件就返回。
retVal = kCFRunLoopRunHandledSource; // 4
} else if (timeout_context->termTSR < mach_absolute_time()) {
/// 超出传入参数标记的超时时间了
retVal = kCFRunLoopRunTimedOut; // 3
} else if (__CFRunLoopIsStopped(rl)) {
/// 被外部调用者强制停止了
__CFRunLoopUnsetStopped(rl); // 2
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
// 调用了_CFRunLoopStopMode将mode停止了
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped; // 2
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
// source0/source1/timers/blocks一个都没有了
retVal = kCFRunLoopRunFinished; // 1
}
}while(0 == retVal)
__CFRunLoopDoBlocks
解析关于runloop调用的blocks,而Source0、Source1、timers以及Observers 在前面已经解析过了。
这个方法主要处理Blocks回调。
- runloop对象有一个_block_item结构的链表,里面存储着当前runloop待处理的blocks
- 执行
__CFRunLoopDoBlocks
方法就是遍历runloop的链表,取出blocks,然后执行__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
方法 - KVO回调方法,以及在主线程调用的block;这两种情况回调由
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
方法调用执行
CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE(msg);
- 在调用GCD的异步函数
dispatch_async
,往主线程里添加任务时会触发该方法 - 由于子线程默认是没有开启Runloop的,子线程下执行GCD的任务是不会被添加到RunLoop上的,子线程blocks的执行是由
lidbdispatch
驱动完成的。
RunLoop的应用
AutoreleasePool
App启动后,苹果在主线RunLoop里注册了两个观察者,分别观察RunLoop的即将进入loop事件、即将进入休眠事件、即将退出事件。
- 即将进入loop事件触发时会调用
_objc_autoreleasePoolPush()
方法创建自动释放池以及插入哨兵对象 - 即将进入休眠事件触发时会分别调用
_objc_autoreleasePoolPop()
和_objc_autoreleasePoolPush()
方法,释放旧的自动释放池并release里面的对象以及创建新的自动释放池 - 即将退出事件触发时会调用
_objc_autoreleasePoolPop()
方法,释放自动释放池。
NSTimer
NSTimer
就是上文提到的timer
- 底层
Runloop
会在每个时间点都注册一个事件,到了事件点进行回调,但是并不是每次都是很准确地在指定时间点进行回调的,timer
中有一个属性Tolerance
标识可以容忍多大的误差。 - 如果
RunLoop
有很多要处理的事,错过了timer
指定的时间点,那么是会错过此次回调的。 - 默认在主线创建的
timer
都会自动加入到RunLoop
中,而如果是在子线程中创建的timer
,在没有开启RunLoop
的情况下,其实是无效的。
CADisplayLink
-
CADisplayLink
是一个执行频率(fps)和屏幕刷新相同的定时器,需要加入到RunLoop才能执行。但是我们也可以通过调用API来实现更改执行频率。 - 它与
NSTimer
都是定时器,区别在于CADisplayLink
的精度更高,而NSTimer
的使用范围更加地广泛。
事件响应
- 苹果注册了一个 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 事件都是在这个回调中完成的。
手势识别
- 当上面的
_UIApplicationHandleEventQueue()
识别了一个手势时,其首先会调用Cancel
将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。 - 苹果注册了一个
Observer
监测BeforeWaiting
(Loop即将进入休眠) 事件,这个Observer的回调函数是_UIGestureRecognizerUpdateObserver()
,其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。 - 当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。
UI更新
当修改了View的frame等导致View的图层发生了变化或者手动调用了setNeedsDisplay/setNeedsLayout
方法之后就会将这个View标记放进一个全局容器里面,当RunLoop
的即将进入休眠或者退出事件回调时,就会遍历这个全局容器将UI进行绘制更新。
PerformSelector 的实现原理
- 当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
- 当调用 performSelector:onThread: 时,触发的是Source0事件,同样的,如果对应线程没有 RunLoop 该方法也会失效。