1. 程式人生 > >RunLoop 四: RunLoop原始碼學習

RunLoop 四: RunLoop原始碼學習

一、前言

  • 因為RunLoop的原始碼涉及到C語言,所以比較抽象。
  • 在蘋果網站上下載 RunLoop 的原始碼,地址為:https://opensource.apple.com/tarballs/CF/
  • 找到 CFRunLoop.c 檔案,進行學習。
  • 需要找到RunLoop 的入口檔案,也就是上個筆記上寫的 ‘通知Observers:進入Loop’ 這句話。

二、如何找到 RunLoop 的入口

  1. 新建一個iOS專案,在ViewController.m 檔案中 寫一個 touchesBegan:withEvent 方法,在這個方法中打上斷點.
  2. 為何在 這個方法中打上斷點,因為 點選事件就是由 RunLoop來處理的。在這個方法中打上斷點,就可以看到到底是RunLoop的哪一個函式來進行呼叫的。也就是要在 這個方法中看下函式呼叫棧。
  3. 函式呼叫棧
    函式呼叫棧
  4. 可以看到函式呼叫棧是從下往上開始查詢的。最開始的入口是 UIApplicationMain, 依次是:GSEventRunModalCFRunLoopRunSpecific__CFRunLoopRun__CFRunLoopDoSources0__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__、直到 呼叫 touchesBegan:withEvent:方法
  5. 我們可以在runloop.c檔案中查詢 CFRunLoopRunSpecific函式。也就是 用 CFRunLoopRunSpecific
    函式當 runloop的入口。
  6. 可以在CFRunLoop.c檔案中搜索到函式: SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled)

三、理順 CFRunLoopRunSpecific 函式的流程
注: 只保留 CFRunLoopRunSpecific 中主要的程式碼。

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    
    // 通知 Observers: 進入 Loop
    // currentMode : 進入到某種模式
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    
    // 具體要做的事情
    // __CFRunLoopRun 這個可以在函式棧中看到。
	result  == __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    
    // 通知 Observers: 退出 Loop
     __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

    return result;
}

四、__CFRunLoopRun 函式做了哪些事情

  1. 在這個函式中你會看到一個 do { ... ... } while(0 == retVal)的迴圈。這個迴圈的含義是,如果 do 後面的大括號 裡面的程式碼返回值等於 0 (也就是 while 條件成立的話),會一直執行 do 裡面的程式碼
  2. 如果 條件一直成立,它就會一直迴圈的做do裡面的事情。
  3. do while 迴圈 裡面的內容,就是 runloop 具體要做的事情
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    
    int32_t retVal = 0;
    
    do {

        // 通知 Observers: 即將處理 Timers
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        
        // 通知 Observers: 即將處理Sources
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);

        // 處理 blocks
        __CFRunLoopDoBlocks(rl, rlm);

        // 處理 source0
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        // 如果返回 Yes ,再次處理blcoks
        if (sourceHandledThisLoop) {
            // 處理 blocks
            __CFRunLoopDoBlocks(rl, rlm);
        }
        
        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);

        // 判斷 有沒有 Sources1
        if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
            // 如果有 Sources1, 就跳轉到 handle_msg
            goto handle_msg;
        }

        didDispatchPortLastTime = false;

        // 通知 Observers: 即將休眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        __CFRunLoopSetSleeping(rl);
	
        do {
            // 等待別的訊息來喚醒當前執行緒
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
            // 如果沒有訊息來喚醒,就會阻塞在這裡。一直等到訊息來喚醒才會往下執行程式碼。
        } while (1);
        
        __CFRunLoopUnsetSleeping(rl);
        // 通知 Observers:結束休眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

    handle_msg:
       if (被timer喚醒) {
           // 處理 Timers
           __CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
        } else if (被 GCD 喚醒) {
           // 處理 GCD
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
        } else {// 被 Source1 喚醒
            // 處理 Source1
            __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
        }
        // 處理 blocks
        __CFRunLoopDoBlocks(rl, rlm);
        
        // 設定返回值
        if (sourceHandledThisLoop && stopAfterHandle) {
            retVal = kCFRunLoopRunHandledSource;
            } else if (timeout_context->termTSR < mach_absolute_time()) {
                retVal = kCFRunLoopRunTimedOut;
        } else if (__CFRunLoopIsStopped(rl)) {
                __CFRunLoopUnsetStopped(rl);
            retVal = kCFRunLoopRunStopped;
        } else if (rlm->_stopped) {
            rlm->_stopped = false;
            retVal = kCFRunLoopRunStopped;
        } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
            retVal = kCFRunLoopRunFinished;
        }
        
    } while (0 == retVal);

    return retVal;
}

五、 RunLoop 每個階段裡面所呼叫的函式
在這裡插入圖片描述

每一個步驟裡面都會呼叫一個很長的方法,這個方法是處理 UIKit 的。
01、通知Observers:進入Loop:

  • __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__

04、處理Blocks:

  • __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__

08、通知Observers:結束休眠(被某個訊息喚醒)

  • 01> 處理Timer:
    • __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
  • 02> 處理GCD Async To Main Queue :
    • __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
  • 03> 處理Source1 :
    • __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__

09、處理Blocks

  • __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__

例如:
(一)、 NSTimer

  • 當到了_CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__,在上面的 #1 則是 Foundation 框架的 NSFireTimer 了。
    在這裡插入圖片描述

(二)、特殊的GCD

  • 一般來說 GCD 有自己的邏輯來處理。
  • GCD 有很多東西是不依賴 RunLoop 處理的。
  • 也就是說: GCD 是 GCD ; RunLoop 是 RunLoop 。 他們是分開的。
  • 但GCD 有一種情況,會交給 RunLoop 來處理。如下圖:
    RunLoop處理GCD
  • 列印它的函式棧
    GCD的函式棧

六、休眠的細節

(一)、複習

  1. 07:通知Observers:開始休眠(等待訊息喚醒) 這條如果沒有訊息,就開始睡覺。
  2. 開始睡覺,可以當成是執行緒阻塞,不會繼續往下走。
  3. 程式碼為:
        do {
            // 等待別的訊息來喚醒當前執行緒
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
            // 如果沒有訊息來喚醒,就會阻塞在這裡。一直等到訊息來喚醒才會往下執行程式碼。
        } while (1);
  1. 休眠意味著當前執行緒不做事情,CPU 不會給 當前執行緒任何資源,當前執行緒就沒有事情可做,就睡覺。程式碼就會停留在這個位置,不會再往下走。
  2. 一旦使用者喚醒它,才會繼續往下走。執行緒開始做事情。

(二)、執行緒阻塞是如何實現的

  1. 如果 寫成 while(1){... ... } 形式的執行緒阻塞,是通過程式碼進行阻塞,當前執行緒並沒有休息。因為這句程式碼是死迴圈,一直在判斷條件,條件成句,執行語句;條件成句,執行語句;一直在執行程式碼,相當於當前執行緒一直在執行程式碼,沒有休息。這樣 cpu 並沒有休息。

  2. __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy) 這句程式碼,會直接讓 當前執行緒睡覺。

  3. 怎麼會達到這種效果?

    • 在 RunLoop.c -> __CFRunLoopServiceMachPort 方法中,有 mach_msg(msg,MACH_RCV_MSG|...)這行程式碼
    • 當看到 mach_msg 代表 核心 函式。它就能直接睡覺。
    • 要想達到直接睡覺,只有核心層面才能辦到。
  4. API 分為:

    • 核心層面API : 非常底層的API,作業系統底層的API。
      • 例如: 讓系統直接死掉的API、讓系統直接休眠的API或者直接操作硬體層面的一些程式碼。
      • 一般不開放給 程式設計師 使用。因為比較危險。
    • 應用層面API :
      • 開放給 程式設計師使用。可以搭建一些介面、傳送網路請求之類的。
  5. RunLoop休眠的實現原理

  • 有兩大類,使用者方面和核心方面。當用戶呼叫mach_msg()方法時,mach_msg() 會直接呼叫 核心裡面的 mach_msg().
  • 核心的 mach_msg()方法等待訊息。如果沒有沒有訊息就讓執行緒休眠,有訊息就喚醒執行緒。
  • 當有訊息的時候,核心的 mach_msg() 會告訴 使用者 , 處理訊息。
    在這裡插入圖片描述

七、面試題:

  1. runloop內部實現邏輯?
  • 就是上邊的執行邏輯圖
  1. runloop和執行緒的關係?
  • 一對一的關係
  1. runloop 是怎麼響應使用者操作的, 具體流程是什麼樣的?
  • 首先是由 Source1 捕捉系統事件。
  • 相當於一旦使用者點選螢幕,先是由 Source1 去處理這個事件,然後 Source1 把這個事件包裝事件佇列 EventQueen,.
  • 事件佇列 EventQueen 是由 source0 處理。
  1. 說說runLoop的幾種狀態說說runLoop的幾種狀態
  • kCFRunLoopEntry: 進入 RunLoop 迴圈
  • kCFRunLoopBeforeTimers: 處理 定時器(Timers
  • kCFRunLoopBeforeSources: 處理 Sources
  • kCFRunLoopBeforeWaiting: 休眠之前
  • kCFRunLoopAfterWaiting: 被喚醒
  • kCFRunLoopExit: 退出當前 RunLoop
  1. runloop的mode作用是什麼?
  • kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的預設Mode,通常主執行緒是在這個Mode下執行
  • UITrackingRunLoopMode:介面跟蹤 Mode,用於 ScrollView 追蹤觸控滑動,保證介面滑動時不受其他 Mode 影響

作用:

  • 隔離。
  • 將不同的Source0/Source1/Timer/Observer隔離開來。
    這樣每一組都不會互相影響。