iOS 淺談 Runloop

swordjoy發表於2019-02-27

RunLoop 是什麼

強烈推薦 ibireme 大神的文章深入理解RunLoop

Runloop原始碼地址

關於 Runloop ,儘管早就知道它的本質實現是一個迴圈,但筆者還是一直很困惑它的作用是什麼 ,不過最近整理相關知識總算是理解了。

程式碼的執行邏輯是自上而下的,如果沒有 Runloop ,程式碼執行完畢後,程式就退出了,對應到實際場景就是 APP 一開啟立馬就退出了。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"程式執行中...");
    }
    return 0;
}
// log
程式執行中...
Program ended with exit code: 0
複製程式碼

例如上面的程式碼,程式碼執行完畢後,main 函式返回,然後程式退出。

為什麼工作中,好像沒有編寫 Runloop 相關的程式碼,程式還是能夠穩定持續執行呢?

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}
複製程式碼

這是因為程式自動幫我們在 UIApplicationMain… 中做了這個事情。

下面來看看 Runloop 的簡化的虛擬碼,主要來自 sunnyxx 大神的一次視訊分享:

function loop() {
    do {
        有事幹了 = 我睡覺了沒事別找我();
        if (搬磚) {
            搬磚();
        } else if (吃飯) {
            吃飯();
        }
    } while (活著)
}
複製程式碼

這個虛擬碼看著還是有一點抽象,需要了解的一個知識點是執行緒和 RunLoop 之間是一一對應的,這裡的睡覺了可以理解為執行緒休眠 [NSThread sleepUntilDate:...]],也就是說當應用沒有任何事件觸發時,就會停在睡覺那行程式碼不執行,這樣就節約了 CPU 的運算資源,提高程式效能,直到有事件喚醒應用為止。例如上面的搬磚事件,吃飯事件。處理完後,又會進入睡覺狀態直到下次喚醒,反覆迴圈,這樣就保證了程式能隨時處理各種事件並能夠穩定執行。

實際上觸控事件、螢幕 UI 重新整理、延遲迴調等等都是 Runloop 實現的。

Runloop 的結構

先來看看 Runloop 的結構原始碼:

struct __CFRunLoop {
    pthread_t _pthread;
    CFMutableSetRef _commonModes;     
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    // ...
};
複製程式碼

這裡包含一個執行緒的成員變數 _pthread,可以看出 Runloop 確實和執行緒是息息相關的。還能看到 Runloop 擁有很多關於 Model 的成員變數,再來看看 Model 的結構:

struct __CFRunLoopMode {
    CFStringRef _name;
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
	// ...
};
複製程式碼

先不管這些東西是幹什麼的,至少我們現在能夠得出如下圖所示的理解:

iOS 淺談 Runloop

一個 Runloop 中包含若干個 Model ,每個 Mode 又包含若干個 Source/Timer/Observer

Runloop 的 Model

Model 代表 Runloop 的執行模式,Runloop 每次只能指定一個 Model 作為 _currentMode ,如果需要切換 Mode,只能退出當前 Loop,再重新選擇一個 Mode 進入。主執行緒的 Runloop 這裡有兩個預置的模式 ,並且這也是系統公開的兩個 Model

  • kCFRunLoopDefaultModeAPP 的普通狀態,通常主執行緒是在這個Mode下執行,已被標記為 Common

  • UITrackingRunLoopModeApp 追蹤觸控 ScrollView 滑動時的狀態,保證介面滑動時不受其他 Mode影響,已被標記為 Common

注意 Runloop 的結構中有一個 _commonModes 。這裡是因為一個 Mode 可以將自己標記為 Common (通過將其 ModeName 新增到 RunLoopcommonModes 中 ),標記為 CommonModel 都可以處理事件,可以理解為變相的實現了多個 Model 同時執行。同時系統也提供了一個操作 Common 標記的字串->kCFRunLoopCommonModes。如果我們想要上面兩種模式下都能處理事件,就可以使用這個字串。

Model 中的 Item

Source/Timer/Observer 被統稱為 mode item,不同 ModelSource0/Source1/Timer/Observer 被分隔開來,互不影響,如果 Mode 裡沒有任何Source0/Source1/Timer/ObserverRunLoop 會立馬退出。

Source

Source 是事件產生的的地方,它對應的類為 CFRunLoopSourceRefSource 有兩個版本:Source0Source1

  • Source0 只包含了一個回撥(函式指標),它並不能主動觸發事件。
  • Source1 包含了一個 mach_port 和一個回撥(函式指標),被用於通過核心和其他執行緒相互傳送訊息。這種 Source 能主動喚醒 RunLoop 的執行緒。例如螢幕觸控、鎖屏和搖晃等。

Timer

Timer 對應的類是 CFRunLoopTimerRef,它其實就是 NSTimer,當其加入到 RunLoop 時,RunLoop會註冊對應的時間點,當時間點到時,RunLoop會被喚醒以執行那個回撥。

Observer

Observer 對應的類是 CFRunLoopObserverRef,當 RunLoop 的狀態發生變化時,觀察者就能通過回撥接受到這個變化。可以觀測的時間點有以下幾個:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0), // 即將進入Loop
    kCFRunLoopBeforeTimers  = (1UL << 1), // 即將處理 Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠
    kCFRunLoopAfterWaiting  = (1UL << 6), // 剛從休眠中喚醒
    kCFRunLoopExit          = (1UL << 7), // 即將退出Loop
};
複製程式碼

Runloop 的內部邏輯

開啟開頭的 Runloop 的原始碼,面對眾多程式碼,讓人毫無頭緒,但是前文中已經講到,螢幕的觸控事件是 Runloop 來處理的。於是打個斷點,來檢視程式的函式呼叫棧:

iOS 淺談 Runloop

從圖中能看到,Runloop 是從 11 開始的,於是從原始碼中搜尋 CFRunLoopRunSpecific 函式,這裡只探究內部主要邏輯,其他細節不看,下面是精簡後的函式:

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    // 根據 modeName 獲取currentMode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
	// 設定 Runloop 的 Model
    CFRunLoopModeRef previousMode = rl->_currentMode;
    rl->_currentMode = currentMode;
	// 通知 Observers: 即將進入 RunLoop
	__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    // 進入 runloop
	result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
	// 通知 Observers: RunLoop 即將退出
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    return result;
}
複製程式碼

然後再進入 __CFRunLoopRun(...) 函式檢視內部精簡後的主要邏輯原始碼:

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);
        // 處理 Sources0
        if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) {
            // 處理 Blocks
            __CFRunLoopDoBlocks(rl, rlm);
        }

        // 判斷有無 Sources1
        if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
            // 跳轉到 handle_msg 處理 Sources1soso
            goto handle_msg;
        }
        // 通知 Observers: 即將休眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        // 開始休眠
        __CFRunLoopSetSleeping(rl);

        // 等待訊息喚醒當前執行緒
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
        // 結束休眠
        __CFRunLoopUnsetSleeping(rl);
        // 通知 Observers: 結束休眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

    // 處理
    handle_msg:;
        // 被 timer 喚醒
        if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
            // 處理 timer
            __CFRunLoopDoTimers(rl, rlm, mach_absolute_time())
        }
        // 被 gcd 喚醒
        else if (livePort == dispatchPort) {
            // 處理 gcd
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
        // 被source1喚醒
        } else {
            __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply);
        }

        // 處理 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 內部確實是一個迴圈,並且,喚醒 RunLoop 的方式有 mach portTimerdispatch

。筆者最初在疑惑一個問題,上面的函式呼叫棧是一個點選螢幕後的響應事件,可以看出這裡是 sources0 ,明明是一個觸控事件為什麼不是 sources1 呢,筆者猜測 sources1 這裡喚醒了 Runloop ,因為 sources0 是無法喚醒 runloop 的,然後再在 sources0 的回撥中處理的點選事件。

RunLoop 中的 mach port

這裡由於目前筆者水平有限,只能夠理解到 mach port 是一個可以控制硬體和接受硬體反饋的一個系統,然後可以通過它將來自硬體的操作轉化成熟知的 UIEvent 事件等等。

總結

這篇文章主要講解了 Runloop 到底是一個什麼東西,當然 Runloop 的知識不僅僅只有這篇文章這點。例如實際用處中的執行緒保活(AFNetworking 2.x 版本中),滑動時 Timer 怎麼不被停止,自動釋放池的實現等等都用到了 Runloop

相關文章