關(guān)于runloop,好多人都理解錯(cuò)了!

跟多數(shù)開發(fā)者一樣,我也曾經(jīng)迷惑于runloop,最初只了解可以通過runloop一些監(jiān)聽事件的通知來做一些事情,優(yōu)化性能。關(guān)于runloop源碼的基礎(chǔ)知識(shí),本文不做論述,可以參考眾神的文章:

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


本文主要內(nèi)容:

  • 指出廣泛傳播runloop文章中錯(cuò)誤
  • 通過代碼論證錯(cuò)誤
  • 通過demo論證錯(cuò)誤

runloop解讀文章中的錯(cuò)誤

本人也看著眾神的文章才對(duì)runloop有了比較深入了解,最近自己終于利用零零星星的時(shí)間把runloop源碼也看了一遍,才發(fā)現(xiàn)好多人都誤解了runloop??!就拿下面這張好多文章中都提及的圖片和流程來說:

摘自《深入理解RunLoop》

這是runloop運(yùn)行流程圖,但其實(shí)這個(gè)圖里面有兩個(gè)錯(cuò)誤,請(qǐng)看下面標(biāo)注圖:

錯(cuò)誤標(biāo)注圖
  • 第一個(gè)錯(cuò)誤 “source0(port)” 應(yīng)該是作者筆誤,圖中錯(cuò)誤將source1 (基于port)寫成source0;

  • 第二個(gè)錯(cuò)誤 "5. 如果有source1,跳到第9步" 從圖和作者的代碼注釋中都能看出是理解有錯(cuò)誤,這里也正是本文重點(diǎn)描述的內(nèi)容


先說結(jié)論,再逐步驗(yàn)證:

這里其實(shí)判斷的是 主線程是否有需要處理的事件,如果沒有則跳到第9步,這里跟source1沒有關(guān)系!
所以應(yīng)該改成“5. 如果當(dāng)前是主線程的runloop,并且主線程有事兒,跳到第9步”

源碼論證

我們直接上源碼(版本CF-1151.16)分析一下,直接看這句話對(duì)應(yīng)的代碼(有精簡(jiǎn)):

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;
      }
}

可以看出跳轉(zhuǎn)到第9步(goto handle_msg)的邏輯是判斷__CFRunLoopServiceMachPort函數(shù)的返回值是否為真,而這個(gè)if對(duì)應(yīng)的就是上文描述“如果有source1”,那么這句話是這個(gè)意思嗎? 起初我也是這么認(rèn)為的,直到我看到了后面下一段第7步“休眠”的代碼:

// 第七步,進(jìn)入循環(huán)開始不斷的讀取端口信息,如果端口有喚醒信息則喚醒當(dāng)前runLoop

__CFPortSet waitSet = rlm->_portSet;

...

...


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

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

這里面出現(xiàn)了上面的一樣的__CFRunLoopServiceMachPort方法, 單拎出來比對(duì)下,

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

比較后發(fā)現(xiàn),參數(shù)中第一個(gè)參數(shù)和倒數(shù)第三個(gè)參數(shù)不同。我們通過__CFRunLoopServiceMachPort的源碼來分析下,其中重點(diǎn)關(guān)注:

  • livePort的賦值用于函數(shù)外部使用;
  • __CFRunLoopServiceMachPort方法中mach_msg的參數(shù)MACH_RCV_MSG表示在接收消息;
  • __CFRunLoopServiceMachPort參數(shù)timeout對(duì)于二者入?yún)⒎謩e是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;
}

從代碼中我們可以大概看出,休眠時(shí)調(diào)用這個(gè)方法的作用就是監(jiān)聽判斷waitSet中所有port,如果這些port中有一個(gè)出現(xiàn)消息,就喚醒了跳出休眠,并且將喚醒的port賦值給livePort。對(duì)于上面的mach_msg,我們?cè)诔绦蜻\(yùn)行時(shí)打斷點(diǎn)一定經(jīng)常遇到,如下圖,當(dāng)runloop處于休眠時(shí),就是下面的狀態(tài),也就是上面代碼中mach_msg的timeout入?yún)門IMEOUT_INFINITY時(shí)阻塞式等待的情況:

阻塞等待消息堆棧

下面的代碼也驗(yàn)證了livePort用來判斷是哪種激勵(lì)將休眠喚醒,通過livePort來判斷是進(jìn)行哪種處理:

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) 
{
      ......
      // 處理主線程隊(duì)列中事件
      __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
      ......
}
else 
{
      ......
      // 處理Source1
      sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
      ......
}

通過上面對(duì)__CFRunLoopServiceMachPort的源碼分析:我們基本確定了,第5步對(duì)應(yīng)的代碼

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

其實(shí)__CFRunLoopServiceMachPort在等的是dispatchPort這個(gè)端口的消息,而這個(gè)端口是什么呢? 我們順著源碼向前找:

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();

我們重點(diǎn)看if判斷中的 (CFRunLoopGetMain() == rl),其中rl表示當(dāng)前的runloop,查看CFRunLoopGetMain()源碼可知返回的是主線程的runloop,所以這里判斷就是當(dāng)前runloop是否是主線程的runloop,這時(shí)我們?cè)倩氐较旅嫣D(zhuǎn)到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;
      }
}

我們可以看到判斷是否跳轉(zhuǎn)之前先判斷dispatchPort有沒有消息,而再之前的條件必須滿足MACH_PORT_NULL != dispatchPort,也就是前面必須對(duì)dispatchPort有所賦值,才會(huì)進(jìn)行下面的判斷和跳轉(zhuǎn)邏輯。所以這里可以小總結(jié)一下重要的結(jié)論:

  • 只有當(dāng)前運(yùn)行的runloop是主線程的runloop時(shí),才會(huì)對(duì)dispatchPort賦值;
  • 如果dispatchPort沒有賦值,則不會(huì)進(jìn)行是否“goto handle_msg”的邏輯判斷;
  • dispatchPort賦予的值是主線程隊(duì)列對(duì)應(yīng)的port;
  • 如果當(dāng)前運(yùn)行的runloop不是主線程的runloop,那么原圖中的第5步就不會(huì)存在,也就是多子線程圖中不存在第5步;

綜上,終于來到我們理論的總結(jié):原圖中第5步的應(yīng)該由"5. 如果有source1,跳到第9步"改成“5. 如果當(dāng)前是主線程的runloop,并且主線程有事兒,跳到第9步”。 所以最終整體流程應(yīng)該是:

  1. 通知observer run loop被觸發(fā)
  2. 如果有timers事件的話,通知observer
  3. 如果有source0要處理的話,通知observer
  4. 觸發(fā)所有的準(zhǔn)備完畢的source0
  5. 如果當(dāng)前是主線程的runloop,并且主線程有事兒,跳到第9步
  6. 通知Observer runloop將進(jìn)入sleep狀態(tài)
  7. mach進(jìn)入sleep和監(jiān)聽狀態(tài)
  8. 通知observer,runloop被woke up
  9. 如果runloop是被喚醒,CFRUNLOOP_WAKEUP_FOR_WAKEUP
  10. 如果用戶定義的timer被觸發(fā),處理event并重啟RunLoop
  11. 如果dispatchPort,處理主線程
  12. 如果一個(gè)source1被觸發(fā),__CFRunLoopDoSource1
  13. 繼續(xù)循環(huán)或通知observer runloop將要exited。

demo論證

最后我們?cè)儆胐emo來佐證一下,demo中我會(huì)首先則監(jiān)聽主線程的runloop,然后再在子線程監(jiān)聽子線程的runloop,打印監(jiān)聽的事件。
先看下demo中的主要代碼:

// 添加主線程runloop監(jiān)聽者
[self addMainObserver];

// 添加子線程runloop監(jiān)聽者
[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監(jiān)聽者
- (void)addOtherObserver
{
      [NSThread detachNewThreadWithBlock:^{

      _timer = [NSTimer scheduledTimerWithTimeInterval:3 repeats:NO block:^(NSTimer * _Nonnull timer) 
      {
            NSLog(@"###cmm子線程###timer時(shí)間到");
      }];

      CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
      switch (activity) {
            case kCFRunLoopEntry:
            NSLog(@"###cmm子線程###進(jìn)入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監(jiān)聽者

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

      switch (activity) {

            case kCFRunLoopEntry:
            NSLog(@"###cmm###進(jìn)入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時(shí)間到");
    }];
}

結(jié)合剛才整理的runloop的整體流程分析一下預(yù)期的打印結(jié)果應(yīng)該是:

  • 主線程中,如果有事兒需要處理, “即將處理timer事件”-->"即將處理source事件"-->下一個(gè)循環(huán)的"即將處理timer事件"-->"即將處理source事件",這里沒有經(jīng)過“即將休眠”,就是因?yàn)橹骶€程有事兒,進(jìn)入“goto handle_msg”,直接跳過休眠階段。
  • 子線程在主線程runloop處理事兒的時(shí)候,并沒有打印結(jié)果變化,說明并沒有觸發(fā)這個(gè)goto條件。

demo跑起來~~~
我們?cè)谥骶€程的代碼中打斷點(diǎn),查看堆棧和日志如下圖:

堆棧和日志

可以發(fā)現(xiàn),如我們所料:主線程的runloop在即將處理source事件后,直接跳到了 “__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__” ,也就是跳過了休眠,直接到了handle_msg對(duì)應(yīng)的 else if (livePort == dispatchPort) 分支。另外我們可以在日志中發(fā)現(xiàn)此時(shí)子線程的runloop已經(jīng)啟動(dòng),并處于休眠狀態(tài)。
然后我們注意下下圖:

日志

如圖中箭頭處,在我們程序跳過斷點(diǎn)繼續(xù)執(zhí)行后,并沒有子線程的相關(guān)打印,說明此時(shí)子線程的runloop并不會(huì)管主線程那部分代碼。

完結(jié)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)載:http://www.cocoachina.com/ios/20150601/11970.html RunL...
    Gatling閱讀 1,548評(píng)論 0 13
  • RunLoop 的概念 一般來講,一個(gè)線程一次只能執(zhí)行一個(gè)任務(wù),執(zhí)行完成后線程就會(huì)退出。如果我們需要一個(gè)機(jī)制,讓線...
    Mirsiter_魏閱讀 670評(píng)論 0 2
  • http://www.cocoachina.com/ios/20150601/11970.html RunLoop...
    紫色冰雨閱讀 947評(píng)論 0 3
  • 深入理解RunLoop 由ibireme| 2015-05-18 |iOS,技術(shù) RunLoop 是 iOS 和 ...
    橙娃閱讀 962評(píng)論 1 2
  • 尼伯特臺(tái)風(fēng)來了,今天早點(diǎn)下班走
    尼伯特閱讀 283評(píng)論 0 0

友情鏈接更多精彩內(nèi)容