iOS知识点iOS Developer程序员

关于runloop,好多人都理解错了!

2018-04-28  本文已影响260人  杭研融合通信iOS

跟多数开发者一样,我也曾经迷惑于runloop,最初只了解可以通过runloop一些监听事件的通知来做一些事情,优化性能。关于runloop源码的基础知识,本文不做论述,可以参考众神的文章:

ibireme:《深入理解RunLoop》
sunyawang:《RunLoop系列之源码分析》
xiaoxiaobukuang:《RunLoop》


本文主要内容:


runloop解读文章中的错误

本人也看着众神的文章才对runloop有了比较深入了解,最近自己终于利用零零星星的时间把runloop源码也看了一遍,才发现好多人都误解了runloop!!就拿下面这张好多文章中都提及的图片和流程来说:

摘自《深入理解RunLoop》

这是runloop运行流程图,但其实这个图里面有两个错误,请看下面标注图:

错误标注图

先说结论,再逐步验证:

这里其实判断的是 主线程是否有需要处理的事件,如果没有则调到第9步,这里跟source1没有关系!
所以应该改成“5. 如果当前是主线程的runloop,并且主线程有事儿,跳到第9步”

源码论证

我们直接上源码(版本CF-1151.16)分析一下,直接看这句话对应的代码(有精简):

if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime)
{
      msg = (mach_msg_header_t *)msg_buffer;
      if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) 
      {
              goto handle_msg;
      }
}

可以看出跳转到第9步(goto handle_msg)的逻辑是判断__CFRunLoopServiceMachPort函数的返回值是否为真,而这个if对应的就是上文描述“如果有source1”,那么这句话是这个意思吗? 起初我也是这么认为的,直到我看到了后面下一段第7步“休眠”的代码:

// 第七步,进入循环开始不断的读取端口信息,如果端口有唤醒信息则唤醒当前runLoop

__CFPortSet waitSet = rlm->_portSet;

...

...


if (kCFUseCollectableAllocator) 
{
    memset(msg_buffer, 0, sizeof(msg_buffer));
}

// waitSet 为所有需要监听的port集合, TIMEOUT_INFINITY表示一直等待
msg = (mach_msg_header_t *)msg_buffer;
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);

这里面出现了上面的一样的__CFRunLoopServiceMachPort方法, 单拎出来比对下,

__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy)

比较后发现,参数中第一个参数和倒数第三个参数不同。我们通过__CFRunLoopServiceMachPort的源码来分析下,其中重点关注:

static Boolean __CFRunLoopServiceMachPort(mach_port_name_t port, mach_msg_header_t **buffer, size_t buffer_size, mach_port_t *livePort, mach_msg_timeout_t timeout, voucher_mach_msg_state_t *voucherState, voucher_t *voucherCopy) 
{
      Boolean originalBuffer = true;
      kern_return_t ret = KERN_SUCCESS;

      for (;;) 
      { /* In that sleep of death what nightmares may come ... */
          mach_msg_header_t *msg = (mach_msg_header_t *)*buffer;
          msg->msgh_bits = 0;
          msg->msgh_local_port = port;
          msg->msgh_remote_port = MACH_PORT_NULL;
          msg->msgh_size = buffer_size;
          msg->msgh_id = 0;
          if (TIMEOUT_INFINITY == timeout) { CFRUNLOOP_SLEEP(); } else { CFRUNLOOP_POLL(); }

          ret = mach_msg(msg, MACH_RCV_MSG|(voucherState ? MACH_RCV_VOUCHER : 0)|MACH_RCV_LARGE|((TIMEOUT_INFINITY !=       timeout) ? MACH_RCV_TIMEOUT : 0)|MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0)|MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AV), 0, msg->msgh_size, port, timeout, MACH_PORT_NULL);

          // Take care of all voucher-related work right after mach_msg.
          // If we don't release the previous voucher we're going to leak it.
          voucher_mach_msg_revert(*voucherState);

          // Someone will be responsible for calling voucher_mach_msg_revert. This call makes the received voucher the current one.
          *voucherState = voucher_mach_msg_adopt(msg);
          if (voucherCopy) 
          {
               if (*voucherState != VOUCHER_MACH_MSG_STATE_UNCHANGED) 
                {
                  *voucherCopy = voucher_copy();
                } 
              else
               {
                  *voucherCopy = NULL;
               }
         }

         CFRUNLOOP_WAKEUP(ret);
          if (MACH_MSG_SUCCESS == ret)
           {
                  *livePort = msg ? msg->msgh_local_port : MACH_PORT_NULL;
                  return true;
          }

          if (MACH_RCV_TIMED_OUT == ret) 
            {
                  if (!originalBuffer) free(msg);
                  *buffer = NULL;
                  *livePort = MACH_PORT_NULL;
                  return false;
            }

          if (MACH_RCV_TOO_LARGE != ret) break;

          buffer_size = round_msg(msg->msgh_size + MAX_TRAILER_SIZE);
          if (originalBuffer) *buffer = NULL;
          originalBuffer = false;
          *buffer = realloc(*buffer, buffer_size);
      }

      HALT;
      return false;
}

从代码中我们可以大概看出,休眠时调用这个方法的作用就是监听判断waitSet中所有port,如果这些port中有一个出现消息,就唤醒了跳出休眠,并且将唤醒的port赋值给livePort。对于上面的mach_msg,我们在程序运行时打断点一定经常遇到,如下图,当runloop处于休眠时,就是下面的状态,也就是上面代码中mach_msg的timeout入参为TIMEOUT_INFINITY时阻塞式等待的情况:

阻塞等待消息堆栈

下面的代码也验证了livePort用来判断是哪种激励将休眠唤醒,通过livePort来判断是进行哪种处理:

if (MACH_PORT_NULL == livePort)
{
      CFRUNLOOP_WAKEUP_FOR_NOTHING();
}
else if (livePort == rl->_wakeUpPort)
{
      CFRUNLOOP_WAKEUP_FOR_WAKEUP();
}
else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort)
{
      // 处理timer
}
else if (livePort == dispatchPort) 
{
      ......
      // 处理主线程队列中事件
      __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
      ......
}
else 
{
      ......
      // 处理Source1
      sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
      ......
}

通过上面对__CFRunLoopServiceMachPort的源码分析:我们基本确定了,第5步对应的代码

if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) 
{
      goto handle_msg;
}

其实__CFRunLoopServiceMachPort在等的是dispatchPort这个端口的消息,而这个端口是什么呢? 我们顺着源码向前找:

mach_port_name_t dispatchPort = MACH_PORT_NULL;
Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));

if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) 
  dispatchPort = _dispatch_get_main_queue_port_4CF();

我们重点看if判断中的 (CFRunLoopGetMain() == rl),其中rl表示当前的runloop,查看CFRunLoopGetMain()源码可知返回的是主线程的runloop,所以这里判断就是当前runloop是否是主线程的runloop,这时我们再回到下面跳转到handle_msg那段代码:

if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) 
{
      msg = (mach_msg_header_t *)msg_buffer;
      if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) 
      {
            goto handle_msg;
      }
}

我们可以看到判断是否跳转之前先判断dispatchPort有没有消息,而再之前的条件必须满足MACH_PORT_NULL != dispatchPort,也就是前面必须对dispatchPort有所赋值,才会进行下面的判断和跳转逻辑。所以这里可以小总结一下重要的结论:

综上,终于来到我们理论的总结:原图中第5步的应该由"5. 如果有source1,调到第9步"改成“5. 如果当前是主线程的runloop,并且主线程有事儿,跳到第9步”。 所以最终整体流程应该是:

  1. 通知observer run loop被触发
  2. 如果有timers事件的话,通知observer
  3. 如果有source0要处理的话,通知observer
  4. 触发所有的准备完毕的source0
  5. 如果当前是主线程的runloop,并且主线程有事儿,跳到第9步
  6. 通知Observer runloop将进入sleep状态
  7. mach进入sleep和监听状态
  8. 通知observer,runloop被woke up
  9. 如果runloop是被唤醒,CFRUNLOOP_WAKEUP_FOR_WAKEUP
  10. 如果用户定义的timer被触发,处理event并重启RunLoop
  11. 如果dispatchPort,处理主线程
  12. 如果一个source1被触发,__CFRunLoopDoSource1
  13. 继续循环或通知observer runloop将要exited。

demo论证

最后我们再用demo来佐证一下,demo中我会首先则监听主线程的runloop,然后再在子线程监听子线程的runloop,打印监听的事件。
先看下demo中的主要代码:

// 添加主线程runloop监听者
[self addMainObserver];

// 添加子线程runloop监听者
[self addOtherObserver];

// 此处使用sleep是为了避免使用timer造成runloop的timer事件的干扰。
sleep(3);
dispatch_async(dispatch_get_main_queue(), ^{

    CGFloat randomAlpha = (arc4random() % 100)*0.01;
    [self.view setBackgroundColor:[UIColor colorWithWhite:0.5 alpha:randomAlpha]];
});
...
...

// 添加子线程runloop监听者
- (void)addOtherObserver
{
      [NSThread detachNewThreadWithBlock:^{

      _timer = [NSTimer scheduledTimerWithTimeInterval:3 repeats:NO block:^(NSTimer * _Nonnull timer) 
      {
            NSLog(@"###cmm子线程###timer时间到");
      }];

      CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
      switch (activity) {
            case kCFRunLoopEntry:
            NSLog(@"###cmm子线程###进入kCFRunLoopEntry");
            break;

            case kCFRunLoopBeforeTimers:
            NSLog(@"###cmm子线程###即将处理Timer事件");
            break;

            case kCFRunLoopBeforeSources:
            NSLog(@"###cmm子线程###即将处理Source事件");
            break;

            case kCFRunLoopBeforeWaiting:
            NSLog(@"###cmm子线程###即将休眠");
            break;

            case kCFRunLoopAfterWaiting:
            NSLog(@"###cmm子线程###被唤醒");
            break;

            case kCFRunLoopExit:
            NSLog(@"###cmm子线程###退出RunLoop");
            break;

            default:
            break;
        }
    });

      CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

      [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
      CFRunLoopRun();
   }];
}

// 添加主线程runloop监听者

- (void)addMainObserver
{
      CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {

      switch (activity) {

            case kCFRunLoopEntry:
            NSLog(@"###cmm###进入kCFRunLoopEntry");
            break;

            case kCFRunLoopBeforeTimers:
            NSLog(@"###cmm###即将处理Timer事件");
            break;

            case kCFRunLoopBeforeSources:
            NSLog(@"###cmm###即将处理Source事件");
            break;

            case kCFRunLoopBeforeWaiting:
            NSLog(@"###cmm###即将休眠");
            break;

            case kCFRunLoopAfterWaiting:
            NSLog(@"###cmm###被唤醒");
            break;

            case kCFRunLoopExit:
            NSLog(@"###cmm###退出RunLoop");
            break;

            default:
            break;
           }
      });

      CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

      _timer1 = [NSTimer scheduledTimerWithTimeInterval:3 repeats:NO block:^(NSTimer * _Nonnull timer) {
      NSLog(@"###cmm###timer时间到");
    }];
}

结合刚才整理的runloop的整体流程分析一下预期的打印结果应该是:

demo跑起来~~~
我们在主线程的代码中打断点,查看堆栈和日志如下图:

堆栈和日志

可以发现,如我们所料:主线程的runloop在即将处理source事件后,直接跳到了 “__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__” ,也就是跳过了休眠,直接到了handle_msg对应的 else if (livePort == dispatchPort) 分支。另外我们可以在日志中发现此时子线程的runloop已经启动,并处于休眠状态。
然后我们注意下下图:

日志

如图中箭头处,在我们程序跳过断点继续执行后,并没有子线程的相关打印,说明此时子线程的runloop并不会管主线程那部分代码。

完结。

上一篇下一篇

猜你喜欢

热点阅读