runtime runoop

iOS RunLoop解析

2021-11-28  本文已影响0人  Fat_Blog

RunLoop

RunLoop概念

RunLoop理解为运行循环。其本质就是一个do-while,这里的do-while和普通的do-while循环不一样,一般的 while 循环会导致 CPU 进入忙等待状态,而 Runloop 则是一种“闲”等待,当没有事件时,Runloop 会进入休眠状态,有事件发生时, Runloop 会去找对应的 Handler 处理事件。Runloop 可以让线程在需要做事的时候忙起来,不需要的话就让线程休眠。

image.png

图中展现了 Runloop 在线程中的作用:从 input source 和 timer source 接受事件,然后在线程中处理事件。

RunLoop 作用

  1. 保持程序持续运行。程序启动就会自动开启一个runloop在主线程
  2. 处理App中的各种事件
  3. 节省CPU资源,提高程序性能 有事情则做事,没事情则休眠

RunLoop 和线程之间的关系

底层RunLoop的存储使用的是字典结构,线程是key,对应的runloop是value。

  1. 主线程是默认开启RunLoop的
  2. 子线程是默认不开启RunLoop的
  3. 子线程开启RunLoop需要我们手动开启,手动开启时会先在全局的存储字典里根据传入的线程key,查看value是否存在,不存在的话就基于线程为key创建一个新的runloop并存储进字典里。

RunLoop Mode

RunLoop Mode可以理解为RunLoop的运行模式,在苹果文档里定义了有五种运行模式。

- kCFRunLoopDefaultMode, App的默认运行模式,通常主线程是在这个运行模式下运行
- UITrackingRunLoopMode, 跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响)
- kCFRunLoopCommonModes, 伪模式,不是一种真正的运行模式
- UIInitializationRunLoopMode:在刚启动App时第进入的第一个Mode,启动完成后就不再使用
- GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到

在源码里搜索__CFRunLoopMode可以看到里面的内部结构,提取出关键的成员变量。

  1. _name:Mode的名字
  2. _sources0:
    1. App内部事件,由App自己管理的,像UIEvent、CFSocket、以及performSelector不带afterDelay参数的方法都是source0
    2. source0不能主动触发,需要调用CFRunLoopWakeUp(runloop) 来唤醒 RunLoop
  3. _sources1:
    1. 由RunLoop和内核管理,Mach port驱动,如CFMachPort、CFMessagePort。
    2. source1包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息
    3. 能主动唤醒 RunLoop 的线程
  4. _observers:添加的观察者
  5. _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,可以发现

  1. RunLoop和线程是绑定的,每个线程会有对应的RunLop,只是主线程是默认开启的,子线程不是默认开启的,需要手动开启,然后底层就会根据传入的线程创建新的RunLoop
  2. _currentMode:当前运行的Mode
  3. _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;
};

通过上面的整理可以发现

  1. RunLoop管理着多个mode
  2. 每次RunLoop运行的的时候都必须指定一个mode作为_currentMode,如果需要执行其他mode的事务,那么就要先退出当前mode,然后切换另一个mode
  3. mode里面管理着source0、source1、timers、observers以及port端口的事务。

引用来自掘金博主的图片🌟


image.png

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的处理逻辑理为

  1. 通知观察者,即将进入runloop
  2. 通知观察者,即将处理Timers
  3. 通知观察者,即将处理Sources
  4. 执行加入到runloop的blocks
  5. 处理Source0,处理之后可能会再次处理blocks
  6. 如果存在Source1,直接跳转到第9步
  7. 通知观察者,即将进入休眠
  8. 通知观察者,结束休眠
  9. 执行唤醒RunLoop的事件
    1. 处理timer事件
    2. 执行gcd通过异步函数提交任务到主线程的Block
    3. 处理Source1事件
    4. 被手动唤醒,不需要执行事件,单纯起到唤醒RunLoop的功能
  10. 执行加入到runloop的blocks
  11. 根据以下情况来确定是否要退出当前RunLoop
    1. 进入run方法时参数表明处理完事件就返回。
    2. 超出run方法参数中的超时时间
    3. 被外部调用者强制停止了
    4. 调用_CFRunLoopStopMode将mode停止了
    5. 检测还有没有待处理的sources/timer/observer
    6. 以上🈚五种情况均无的话 那么就跳转到第2步,继续循环
  12. 通知观察者,退出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回调。

CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE(msg);

RunLoop的应用

AutoreleasePool

App启动后,苹果在主线RunLoop里注册了两个观察者,分别观察RunLoop的即将进入loop事件、即将进入休眠事件、即将退出事件。

NSTimer

NSTimer就是上文提到的timer

CADisplayLink

事件响应

手势识别

UI更新

当修改了View的frame等导致View的图层发生了变化或者手动调用了setNeedsDisplay/setNeedsLayout方法之后就会将这个View标记放进一个全局容器里面,当RunLoop的即将进入休眠或者退出事件回调时,就会遍历这个全局容器将UI进行绘制更新。

PerformSelector 的实现原理

上一篇下一篇

猜你喜欢

热点阅读