runloop你理解对了吗

点击上方“iOS开发”,选择“置顶公众号”

关键时刻,第一时间送达!

640?wxfrom=5&wx_lazy=1

640?wx_fmt=gif&wxfrom=5&wx_lazy=1


本文主要内容:

  • 指出广泛传播runloop文章中错误

  • 通过代码论证错误

  • 通过demo论证错误



runloop解读文章中的错误


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


640?wx_fmt=png&wxfrom=5&wx_lazy=1

摘自《深入理解RunLoop》


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


640?wx_fmt=png

错误标注图


  • 第一个错误 “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时阻塞式等待的情况:


640?wx_fmt=png

阻塞等待消息堆栈


下面的代码也验证了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跑起来~~~


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


640?wx_fmt=png

堆栈和日志


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


然后我们注意下下图:


640?wx_fmt=png

日志


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




640?

  • 作者:杭研融合通信iOS

  • 链接:https://www.jianshu.com/p/ae0118f968bf

  • iOS开发整理发布,转载请联系作者授权

640?wx_fmt=gif640?【点击成为Java大神】

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值