iOS 淺談 Runloop

RunLoop 是什麼

強烈推薦 ibireme 大神的文章深刻理解RunLoopc++

Runloop源碼地址objective-c

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

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

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

例如上面的代碼,代碼執行完畢後,main 函數返回,而後程序退出。函數

爲何工做中,好像沒有編寫 Runloop 相關的代碼,程序仍是可以穩定持續運行呢?oop

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

這是由於程序自動幫咱們在 UIApplicationMain… 中作了這個事情。性能

下面來看看 Runloop 的簡化的僞代碼,主要來自 sunnyxx 大神的一次視頻分享:spa

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;
	// ...
};
複製代碼

先無論這些東西是幹什麼的,至少咱們如今可以得出以下圖所示的理解:

一個 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 來處理的。因而打個斷點,來查看程序的函數調用棧:

從圖中能看到,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

相關文章
相關標籤/搜索