iOS 淺談 Runloop

RunLoop 是什么

強(qiáng)烈推薦 ibireme 大神的文章深入理解RunLoop

Runloop源碼地址

關(guān)于 Runloop ,盡管早就知道它的本質(zhì)實(shí)現(xiàn)是一個(gè)循環(huán),但筆者還是一直很困惑它的作用是什么 ,不過最近整理相關(guān)知識(shí)總算是理解了。

代碼的執(zhí)行邏輯是自上而下的,如果沒有 Runloop ,代碼執(zhí)行完畢后,程序就退出了,對(duì)應(yīng)到實(shí)際場(chǎng)景就是 APP 一打開立馬就退出了。

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

例如上面的代碼,代碼執(zhí)行完畢后,main 函數(shù)返回,然后程序退出。

為什么工作中,好像沒有編寫 Runloop 相關(guān)的代碼,程序還是能夠穩(wěn)定持續(xù)運(yùn)行呢?

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

這是因?yàn)槌绦蜃詣?dòng)幫我們?cè)?UIApplicationMain… 中做了這個(gè)事情。

下面來看看 Runloop 的簡(jiǎn)化的偽代碼,主要來自 sunnyxx 大神的一次視頻分享:

function loop() {
    do {
        有事干了 = 我睡覺了沒事別找我();
        if (搬磚) {
            搬磚();
        } else if (吃飯) {
            吃飯();
        }
    } while (活著)
}

這個(gè)偽代碼看著還是有一點(diǎn)抽象,需要了解的一個(gè)知識(shí)點(diǎn)是線程和 RunLoop 之間是一一對(duì)應(yīng)的,這里的睡覺了可以理解為線程休眠 [NSThread sleepUntilDate:...]],也就是說當(dāng)應(yīng)用沒有任何事件觸發(fā)時(shí),就會(huì)停在睡覺那行代碼不執(zhí)行,這樣就節(jié)約了 CPU 的運(yùn)算資源,提高程序性能,直到有事件喚醒應(yīng)用為止。例如上面的搬磚事件,吃飯事件。處理完后,又會(huì)進(jìn)入睡覺狀態(tài)直到下次喚醒,反復(fù)循環(huán),這樣就保證了程序能隨時(shí)處理各種事件并能夠穩(wěn)定運(yùn)行。

實(shí)際上觸摸事件、屏幕 UI 刷新、延遲回調(diào)等等都是 Runloop 實(shí)現(xiàn)的。

Runloop 的結(jié)構(gòu)

先來看看 Runloop 的結(jié)構(gòu)源碼:

struct __CFRunLoop {
    pthread_t _pthread;
    CFMutableSetRef _commonModes;     
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    // ...
};

這里包含一個(gè)線程的成員變量 _pthread,可以看出 Runloop 確實(shí)和線程是息息相關(guān)的。還能看到 Runloop 擁有很多關(guān)于 Model 的成員變量,再來看看 Model 的結(jié)構(gòu):

struct __CFRunLoopMode {
    CFStringRef _name;
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    // ...
};

先不管這些東西是干什么的,至少我們現(xiàn)在能夠得出如下圖所示的理解:

image

一個(gè) Runloop 中包含若干個(gè) Model ,每個(gè) Mode 又包含若干個(gè) Source/Timer/Observer。

Runloop 的 Model

Model 代表 Runloop 的運(yùn)行模式,Runloop 每次只能指定一個(gè) Model 作為 _currentMode ,如果需要切換 Mode,只能退出當(dāng)前 Loop,再重新選擇一個(gè) Mode 進(jìn)入。主線程的 Runloop 這里有兩個(gè)預(yù)置的模式 ,并且這也是系統(tǒng)公開的兩個(gè) Model

  • kCFRunLoopDefaultModeAPP 的普通狀態(tài),通常主線程是在這個(gè)Mode下運(yùn)行,已被標(biāo)記為 Common。

  • UITrackingRunLoopModeApp 追蹤觸摸 ScrollView 滑動(dòng)時(shí)的狀態(tài),保證界面滑動(dòng)時(shí)不受其他 Mode影響,已被標(biāo)記為 Common

注意 Runloop 的結(jié)構(gòu)中有一個(gè) _commonModes 。這里是因?yàn)橐粋€(gè) Mode 可以將自己標(biāo)記為 Common (通過將其 ModeName 添加到 RunLoopcommonModes 中 ),標(biāo)記為 CommonModel 都可以處理事件,可以理解為變相的實(shí)現(xiàn)了多個(gè) Model 同時(shí)運(yùn)行。同時(shí)系統(tǒng)也提供了一個(gè)操作 Common 標(biāo)記的字符串->kCFRunLoopCommonModes。如果我們想要上面兩種模式下都能處理事件,就可以使用這個(gè)字符串。

Model 中的 Item

Source/Timer/Observer 被統(tǒng)稱為 mode item,不同 ModelSource0/Source1/Timer/Observer 被分隔開來,互不影響,如果 Mode 里沒有任何Source0/Source1/Timer/Observer,RunLoop 會(huì)立馬退出。

Source

Source 是事件產(chǎn)生的的地方,它對(duì)應(yīng)的類為 CFRunLoopSourceRef。Source 有兩個(gè)版本:Source0Source1

  • Source0 只包含了一個(gè)回調(diào)(函數(shù)指針),它并不能主動(dòng)觸發(fā)事件。
  • Source1 包含了一個(gè) mach_port 和一個(gè)回調(diào)(函數(shù)指針),被用于通過內(nèi)核和其他線程相互發(fā)送消息。這種 Source 能主動(dòng)喚醒 RunLoop 的線程。例如屏幕觸摸、鎖屏和搖晃等。

Timer

Timer 對(duì)應(yīng)的類是 CFRunLoopTimerRef,它其實(shí)就是 NSTimer,當(dāng)其加入到 RunLoop 時(shí),RunLoop會(huì)注冊(cè)對(duì)應(yīng)的時(shí)間點(diǎn),當(dāng)時(shí)間點(diǎn)到時(shí),RunLoop會(huì)被喚醒以執(zhí)行那個(gè)回調(diào)。

Observer

Observer 對(duì)應(yīng)的類是 CFRunLoopObserverRef,當(dāng) RunLoop 的狀態(tài)發(fā)生變化時(shí),觀察者就能通過回調(diào)接受到這個(gè)變化??梢杂^測(cè)的時(shí)間點(diǎn)有以下幾個(gè):

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

Runloop 的內(nèi)部邏輯

打開開頭的 Runloop 的源碼,面對(duì)眾多代碼,讓人毫無頭緒,但是前文中已經(jīng)講到,屏幕的觸摸事件是 Runloop 來處理的。于是打個(gè)斷點(diǎn),來查看程序的函數(shù)調(diào)用棧:

image

從圖中能看到,Runloop 是從 11 開始的,于是從源碼中搜索 CFRunLoopRunSpecific 函數(shù),這里只探究?jī)?nèi)部主要邏輯,其他細(xì)節(jié)不看,下面是精簡(jiǎn)后的函數(shù):

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

然后再進(jìn)入 __CFRunLoopRun(...) 函數(shù)查看內(nèi)部精簡(jiǎn)后的主要邏輯源碼:

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) {
            // 跳轉(zhuǎn)到 handle_msg 處理 Sources1soso
            goto handle_msg;
        }
        // 通知 Observers: 即將休眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        // 開始休眠
        __CFRunLoopSetSleeping(rl);

        // 等待消息喚醒當(dāng)前線程
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
        // 結(jié)束休眠
        __CFRunLoopUnsetSleeping(rl);
        // 通知 Observers: 結(jié)束休眠
        __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);
        
        // 設(shè)置返回值
        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 內(nèi)部確實(shí)是一個(gè)循環(huán),并且,喚醒 RunLoop 的方式有 mach portTimerdispatch

。筆者最初在疑惑一個(gè)問題,上面的函數(shù)調(diào)用棧是一個(gè)點(diǎn)擊屏幕后的響應(yīng)事件,可以看出這里是 sources0 ,明明是一個(gè)觸摸事件為什么不是 sources1 呢,筆者猜測(cè) sources1 這里喚醒了 Runloop ,因?yàn)?sources0 是無法喚醒 runloop 的,然后再在 sources0 的回調(diào)中處理的點(diǎn)擊事件。

RunLoop 中的 mach port

這里由于目前筆者水平有限,只能夠理解到 mach port 是一個(gè)可以控制硬件和接受硬件反饋的一個(gè)系統(tǒng),然后可以通過它將來自硬件的操作轉(zhuǎn)化成熟知的 UIEvent 事件等等。

總結(jié)

這篇文章主要講解了 Runloop 到底是一個(gè)什么東西,當(dāng)然 Runloop 的知識(shí)不僅僅只有這篇文章這點(diǎn)。例如實(shí)際用處中的線程?;睿ˋFNetworking 2.x 版本中),滑動(dòng)時(shí) Timer 怎么不被停止,自動(dòng)釋放池的實(shí)現(xiàn)等等都用到了 Runloop

?著作權(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)容

  • 這幾天研究了一下iOS的Runloop,看了不少的文章,收獲不少,但是疑問也挺多。所以我就試著去翻譯了并分析總結(jié)了...
    擰發(fā)條鳥xds閱讀 1,504評(píng)論 0 10
  • 1 Runloop機(jī)制原理 深入理解RunLoop http://www.cocoachina.com/ios/2...
    Kevin_Junbaozi閱讀 4,245評(píng)論 4 30
  • Runloop 是和線程緊密相關(guān)的一個(gè)基礎(chǔ)組件,是很多線程有關(guān)功能的幕后功臣。盡管在平常使用中幾乎不太會(huì)直接用到,...
    jackyshan閱讀 10,018評(píng)論 10 75
  • 寫在前面 由于文章比較長(zhǎng),簡(jiǎn)書沒有目錄,讀起來不方便。建議看有目錄版RunLoop從源碼到應(yīng)用全面解析——帶目錄版...
    紙簡(jiǎn)書生閱讀 3,745評(píng)論 1 16
  • 概述 RunLoop作為iOS中一個(gè)基礎(chǔ)組件和線程有著千絲萬縷的關(guān)系,同時(shí)也是很多常見技術(shù)的幕后功臣。盡管在平時(shí)多...
    陽明AI閱讀 1,161評(píng)論 0 17

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