iOS RunLoop 详解

639 阅读5分钟

1、什么是RunLoop

RunLoop 是通过系统内部管理的事件循环管理消息和事件的对象。

Q:runloop 是否等于 while(1) { do something ... }

A:答案是否定

  • while(1) 是一个忙等的状态,需要一直占用资源。
  • runloop 没有消息需要处理时进入休眠状态,消息来了,需要处理时才被唤醒。

2、不同线程对 RunLoop 的差别对待

添加如下代码,点击屏幕 iShot2021-07-07 17.59.33.png 此时可以看到,输出了1、3、2。

这符合预期,没有问题~

变换一下代码,把主线程切换到全局线程 iShot2021-07-07 17.49.05.png Q:可以看到输出只有1、3,并没有出现 test 方法中输出的2,为什么呢?

  • performSelector: 在当前线程的 RunLoop 中开启一个 timer,运行循环到来时执行 timer 的内容。
  • 主线程的 RunLoop 默认开启
  • 子线程的 RunLoop 默认不开启

A:上述代码 performSelector: 在全局线程(子线程)中执行,RunLoop 没有开启,所以没执行。

但是为什么会区别对待呢?这需要往源码里面翻。

3、RunLoop 的结构

打印一下当前的 RunLoop iShot2021-07-07 11.27.14.png 可以看到 NSRunloop 其实是 CFRunLoop 的封装。

下载 CoreFoundation 源码,搜索 runloop可以看到存在一个 CFRunLoop.c 的文件,在里面可以找到结构体 __CFRunLoop 的构成。重要的组成是红框内的四项

iShot2021-07-08 10.35.12.png

  • 一个 RunLoop 对象里面包含了若干个 RunLoopMode
  • RunLoop 内部通过集合容器 _modes 装载这些 RunLoopMode

3-1、RunLoopMode

iShot2021-07-08 10.47.05.png

RunLoopMode 的核心内容是4个数组容器,分别用来装 source0source1observertimer

3-1-1、source0

source0 里面存放的是一个个结构体 __CFRunLoopSource iShot2021-07-08 11.10.54.png

source0:触摸事件处理、performSelector:onThread:touchesBegan:withEvent:断点,使用bt命令输出函数调用栈信息 iShot2021-07-08 11.31.18.png 可以看到 Runloop 正在处理的触摸事件是一个 source0

同理~ iShot2021-07-08 11.40.07.png

3-1-2、source1

source1 里面存放的是一个个结构体 __CFRunLoopSource,负责的是系统事件捕捉、线程间通信

点击屏幕会产生一个系统事件,通过 source1 捕捉后由 Springboard 程序包装成 source0 分发给应用处理,因此我们在 App 内部接收到的触摸事件都是 source0系统事件捕捉.png

线程间通信也是由 source0 处理的。

3-1-2、timers

timer 里面的是 CFRunLoopTimerRef,包括了定时器事件、[performSelector: withObject: afterDelay:] iShot2021-07-08 11.14.15.png

iShot2021-07-08 11.53.07.png 同样的断点和 bt 命令,清晰明了~

3-1-3、observers

observers 里面是监听者,Runloop 状态变更会通知监听者进行函数回调 iShot2021-07-08 11.16.38.png Runloop 有以下状态 iShot2021-07-08 14.22.41.png 例如监听到 Runloop 状态为 BeforeWaiting 就会刷新UI界面 UI更新.png

3-2、_currentMode

RunLoop 对象内部的 _currentMode 指向了该 RunLoop 的其中一个RunLoopMode,就是当前运行中的 RunLoopMode

RunLoop 当前只会执行 _currentMode 包含的事件(source0source1observertimer)。

3-3、_commonModes

这么看起来只需要 modes 就满足需求了,为什么还需要 _commonModes_commonModeItems 呢?

看一个常见实例:启动一个定时器,每秒执行 test 方法输出,在视图上添加一个可滚动的滚动视图。 iShot2021-07-08 14.29.39.png 不滑动视图时输出正常,但是当我们滑动视图时输出停止了。

定时器 NSTimer 每经过设置的间隔,往当前线程的 RunLoop 的其中一个 Mode 添加timer 事件并存入 timers数组。处理 timer事件 的时候就调用 timer 绑定的 func 或者 block

RunLoop 内部有多个 RunLoopMode,其中两个需要重点关注

  • NSDefaultRunLoopMode:默认Mode
  • NSEventTrackingRunLoopMode:界面追踪Mode

触摸事件会放到 NSEventTrackingRunLoopMode 的事件容器里,所以当用户操作屏幕界面时,App 会切换到该 NSEventTrackingRunLoopMode 下运行,处理触摸事件。

通过 scheduledTimerWithTimeInterval 增加的 timer事件 默认放到了 NSDefaultRunLoopMode,不进行触摸操作时 RunLoop 在运行这个它,所以正常处理添加的timer事件

滑动的时候 RunLoop 就会处理 NSEventTrackingRunLoopMode 的事件,就处理不了 NSDefaultRunLoopMode 里面的 timer事件 了。

如果需要滑动的过程中同时处理定时器事件,就需要把 timer 添加到 NSRunLoopCommonModes

[[NSRunLoop currentRunLoop] addTimer:timer forMode:kCFRunLoopDefaultMode];
  • NSRunLoopCommonModes 模式等效于 NSDefaultRunLoopModeNSEventTrackingRunLoopMode 的结合

  • NSRunLoopCommonModes 不是一个具体的模式,它可以理解成一个标签。

  • 打上标签的 RunLoopMode 会被放入到 RunLoop 内部的 _commonModes

此时 timer 会被放入 RunLoop_commonModeItems 里。只要运行到 _commonModes 所包含的某个 RunLoopMode ,就会去处理 _commonModeItems里面的事件,RunLoopMode 本身的事件也会处理。

4、RunLoop 的运行流程

iShot2021-07-08 15.14.43.png 通过断点找到入口函数

4-1、CFRunLoopRunSpecific

原函数太烦,抽取重要部分看伪代码吧~

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */

    //通知observer 状态切换到 kCFRunLoopEntry
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);

    //启动runloop
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);

    //通知observer 状态切换到 kCFRunLoopExit
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
 
    return result;
}

4-2、__CFRunLoopRun

原函数太烦,抽取重要部分看伪代码吧~

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {

    //退出循环的标签
    int32_t retVal = 0;

    //runloop的核心是do-while循环
    do {
        
      // 2、通知observer 状态切换到 kCFRunLoopBeforeTimers 即将处理Timer
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);

      //3、通知observer 状态切换到 kCFRunLoopBeforeSources 即将处理Source0
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);

      // 处理Blocks
      __CFRunLoopDoBlocks(rl, rlm);

      //4、处理source0
      Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
      if (sourceHandledThisLoop) {
            // 需要的话处理Blocks
            __CFRunLoopDoBlocks(rl, rlm);
      }
        
      //5、判断存在/处理source1
      if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
          //存在source1,跳转到标签handle_msg处
          goto handle_msg;
      }


      //6、通知observer 状态切换到 kCFRunLoopBeforeWaiting 即将进入休眠
       __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);

      //7、开始休眠
      __CFRunLoopSetSleeping(rl);

      // 等待别的消息来唤醒当前线程
      __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
            
      // 线程唤醒
      __CFRunLoopUnsetSleeping(rl);

     // 结束休眠 通知observer 状态切换到 kCFRunLoopAfterWaiting
      __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

    //处理唤醒事件
    handle_msg:;
        
        if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
            // 被timer唤醒  处理timer
            __CFRunLoopDoTimers(rl, rlm, mach_absolute_time())
        } else if (livePort == dispatchPort) {
            // 被GCD唤醒 处理GCD
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
        } else {
            // 被source1唤醒 处理Source1
            __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
        }

      // 处理Blocks
       __CFRunLoopDoBlocks(rl, rlm);
        
      // 设置返回值retVal
      if (sourceHandledThisLoop && stopAfterHandle) {
          /// 进入时传参:处理完事件立刻返回
          retVal = kCFRunLoopRunHandledSource;
      } else if (timeout_context->termTSR < mach_absolute_time()) {
          /// 超时
          retVal = kCFRunLoopRunTimedOut;
      } else if (__CFRunLoopIsStopped(rl)) {
          /// 被外部调用者强制停止了
          __CFRunLoopUnsetStopped(rl);
          retVal = kCFRunLoopRunStopped;
      } else if (rlm->_stopped) {
          /// 自动停止了
          rlm->_stopped = false;
          retVal = kCFRunLoopRunStopped;
      } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
          /// source / timer / observer 都处理完了
          retVal = kCFRunLoopRunFinished;
      }
        /// 没超时,还有mode需要处理,loop没被停止,继续循环。
  } while (0 == retVal);
    
  return retVal;
}

4-3 流程图

总结代码为流程图如下: RunLoop.png

5、大纲

iShot2021-07-08 15.53.35.png