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

跟多数开发者一样,我也曾经迷惑于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并不会管主线程那部分代码。

完结。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容