美文网首页iOS知识点iOS Developer程序员
关于runloop,好多人都理解错了!

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

作者: 杭研融合通信iOS | 来源:发表于2018-04-28 16:29 被阅读260次

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

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


    本文主要内容:

    • 指出广泛传播runloop文章中错误
    • 通过代码论证错误
    • 通过demo论证错误

    runloop解读文章中的错误

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

    摘自《深入理解RunLoop》

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

    错误标注图
    • 第一个错误 “source0(port)” 应该是作者笔误,图中错误将source1 (基于port)写成source0;

    • 第二个错误 "5. 如果有source1,跳到第9步" 从图和作者的代码注释中都能看出是理解有错误,这里也正是本文重点描述的内容


    先说结论,再逐步验证:

    这里其实判断的是 主线程是否有需要处理的事件,如果没有则调到第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的源码来分析下,其中重点关注:

    • livePort的赋值用于函数外部使用;
    • __CFRunLoopServiceMachPort方法中mach_msg的参数MACH_RCV_MSG表示在接收消息;
    • __CFRunLoopServiceMachPort参数timeout对于二者入参分别是0和TIMEOUT_INFINITY,分别表示查询到立刻返回和一直等待有消息再返回;
    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有所赋值,才会进行下面的判断和跳转逻辑。所以这里可以小总结一下重要的结论:

    • 只有当前运行的runloop是主线程的runloop时,才会对dispatchPort赋值;
    • 如果dispatchPort没有赋值,则不会进行是否“goto handle_msg”的逻辑判断;
    • dispatchPort赋予的值是主线程队列对应的port;
    • 如果当前运行的runloop不是主线程的runloop,那么原图中的第5步就不会存在,也就是多子线程图中不存在第5步;

    综上,终于来到我们理论的总结:原图中第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的整体流程分析一下预期的打印结果应该是:

    • 主线程中,如果有事儿需要处理, “即将处理timer事件”-->"即将处理source事件"-->下一个循环的"即将处理timer事件"-->"即将处理source事件",这里没有经过“即将休眠”,就是因为主线程有事儿,进入“goto handle_msg”,直接跳过休眠阶段。
    • 子线程在主线程runloop处理事儿的时候,并没有打印结果变化,说明并没有触发这个goto条件。

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

    堆栈和日志

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

    日志

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

    完结。

    相关文章

      网友评论

      • Zhui_Do:If a port-based input source is ready and waiting to fire, process the event immediately. Go to step 9.
      • 北溟微个尘:很棒实践出真知

      本文标题:关于runloop,好多人都理解错了!

      本文链接:https://www.haomeiwen.com/subject/cemglftx.html