RunLoop 的深入學(xué)習(xí)

RunLoop 是什么?

RunLoop 是和線程緊密相關(guān)的一個(gè)基礎(chǔ)組件疹尾。顧名思議就是循環(huán)運(yùn)行秘豹。按照 OC 的思路,RunLoop 其實(shí)就是一個(gè)對象术浪,這個(gè)對象管理了其需要處理的事件和消息并提供一個(gè)入口函數(shù)來循環(huán)執(zhí)行事件瓢对。平常,一般的 while 循環(huán)會讓 CPU 處于忙等狀態(tài)胰苏,而 RunLoop 則是一種“閑等”,當(dāng)沒有事件時(shí)醇疼,RunLoop 會進(jìn)入休眠狀態(tài)硕并,有事件發(fā)生時(shí), RunLoop 會找對應(yīng)的 Handler 處理事件秧荆。
邏輯代碼如下:

+ (void) loop {
    [self initialize];
    do {
        id message = [self get_next_Message];
        [self process_message:message];
        
    } while (message != quit);
}

RunLoop 可以保持程序的正常運(yùn)行倔毙,可以處理 APP 的各種事件(比如觸摸、定時(shí)器等)乙濒。同時(shí)也節(jié)省了 CPU 的資源陕赃、 提高性能。

OS X/iOS 系統(tǒng)中颁股,提供了兩個(gè)對象:

  • CFRunLoopRef : 在 Core Foundation 框架內(nèi)么库,提供了純 C函數(shù)且線程安全的 API
  • NSRunLoop: 基于 CFRunLoop 的封裝,提供了面向?qū)ο蟮?API甘有,但這些線程是不安全的诉儒。

這兩類 API 都可以訪問和使用 RunLoop,但相對來說亏掀,CFRunLoopRef 的性能更高忱反。

首先泛释,看一下官方 RunLoop 結(jié)構(gòu)圖(下圖的Input Source Port 對應(yīng)的是 Source1)


RunLoop 結(jié)構(gòu)圖

注意:圖中出現(xiàn)的 Input Source 和 Timer Source 都是 RunLoop 事件的來源。但是不同之處在于所有的 Timer 都共用一個(gè)端口 "Mode Timer Port" ,而每個(gè)Source1 都有不同的對應(yīng)端口温算。

RunLoop 與線程

CFRunLoop 是基于 pthread 來管理的怜校。每個(gè)線程都有一個(gè)對應(yīng)的 RunLoop 對象。它們之間的關(guān)系保存在一個(gè)全局的 Dictionary 中注竿。

蘋果不允許直接創(chuàng)建 RunLoop,它提供了 CFRunLoopGetMain()CFRunLoopGetCurrent() 這兩個(gè) API 來獲取 RunLoop 對象韭畸。

主線程的 RunLoop 會在應(yīng)用啟動的時(shí)候完成啟動,其他線程的 RunLoop 默認(rèn)并不會開啟蔓搞,需要我們主動獲取胰丁。

RunLoop 的創(chuàng)建是發(fā)生在第一次獲取時(shí),銷毀是在線程結(jié)束喂分。并且你只能在一個(gè)線程的內(nèi)部獲取其 RunLoop(主線程外)锦庸。

RunLoop 相關(guān)類

在 Core Foundation 里有5個(gè)關(guān)于 RunLoop 的類:

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRed

其中,CFRunLoopModeRef 沒有對外暴露蒲祈,只是通過 CFRunLoopRef 的接口進(jìn)行了封裝甘萧。它們之間的關(guān)系如下圖:

RunLoop

一個(gè) RunLoop 包含若干個(gè) Mode,每個(gè) Mode 又包含若干個(gè) Source梆掸、Observer扬卷、Timer。每次調(diào)用 RunLoop 的主函數(shù) __CFRunLoopRun() 時(shí)必須且只能指定一個(gè) Mode,這個(gè) Mode 就被稱為 CurrentMode酸钦;如果需要切換 Mode,只能退出 Loop,在重新指定一個(gè) Mode 進(jìn)入怪得。

CFRunLoopSourceRef 是事件產(chǎn)生的地方。有兩種Source:

  • Source0: 僅包含一個(gè)回調(diào)(函數(shù)指針)卑硫,不能主動觸發(fā)事件徒恋;使用時(shí),你需要先調(diào)用 CFRunLoopSourceSignal(source)欢伏,將這個(gè) Source 標(biāo)記為待處理入挣,然后在調(diào)用 CFRunLoopWakeUp(runloop)來喚醒 RunLoop,讓其處理這個(gè)事件。它負(fù)責(zé) APP 內(nèi)部事件硝拧,由 APP 負(fù)責(zé)管理觸發(fā)径筏,例如 UITouch 事件。
  • Source1: 包含一個(gè) mach_port 和一個(gè)回調(diào)(函數(shù)指針)障陶,被用于通過內(nèi)核和其他線程互相發(fā)送消息滋恬。能夠主動喚醒 RunLoop 的線程。它由操作系統(tǒng)內(nèi)核進(jìn)行管理咸这,例如 CFMessagePort 消息夷恍。

CFRunLoopTimeRef 是基于時(shí)間的觸發(fā)器,它和 NSTimer 是 toll-free bridged 的,可以混用酿雪。它包含一個(gè)時(shí)間長度和一個(gè)回調(diào)(函數(shù)指針)遏暴。當(dāng)其加入到 RunLoop 時(shí),RunLoop 會注冊對應(yīng)的時(shí)間點(diǎn)指黎,當(dāng)時(shí)間點(diǎn)到時(shí)朋凉,RunLoop 會被喚醒以執(zhí)行那個(gè)回調(diào)。

CFRunLoopObserverRef 是觀察者醋安,每個(gè) Observer 都包含了一個(gè)回調(diào)(函數(shù)指針)杂彭,當(dāng) RunLoop 的狀態(tài)發(fā)生變化時(shí),觀察者就能通過回調(diào)接受到這個(gè)變化吓揪。我們可以觀測的狀態(tài)有:

    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
    }

上面的 Source/Timer/Observer 被統(tǒng)稱為 mode item亲怠,一個(gè) item 可以被同時(shí)加入多個(gè) mode。但一個(gè) item 被重復(fù)加入同一個(gè) mode 時(shí)是不會有效果的柠辞。如果一個(gè) mode 中一個(gè) item 都沒有团秽,則 RunLoop 會直接退出,不進(jìn)入循環(huán)叭首。

RunLoop 的 Mode

首先习勤,我們先來了解一下 CFRunLoopMode 和 CFRunLoop 的結(jié)構(gòu):

    struct __CFRunLoopMode {
        CFString _name;     // mode name
        CFMutableSetRef _sources0;
        CFMutableSetRef -sources1;
        CFMutableArrayRef _observers;
        CFMutableArrayRef _timers;
        ···
    };
    struct __CFRunLoop {
        CFMutableSetRef _commonModes;
        CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
        CFRunLoopModeRef _currentMode;    // Current RunLoop Mode
        CFMutableSetRef _modes;          
    }

CommonModes

CommonModes: 一個(gè) Mode 可以通過將其 ModeName 添加到 RunLoop 的 "commonModes" 中,從而將自己標(biāo)記為"Common" 屬性焙格。每當(dāng) RunLoop 的內(nèi)容發(fā)生變化時(shí)图毕,RunLoop 都會自動將_commonModeItems 里的 Source/Observer/Timer 同步到具有"Common"標(biāo)記的所有 Mode 里。

應(yīng)用場景:主線程的 RunLoop 里有兩個(gè)預(yù)置的 Mode:KCFRunLoopDefaultMode 和 UITrackingRunLoopMode眷唉。這兩個(gè) Mode 都已經(jīng)被標(biāo)記為"Common"屬性予颤。DefaultMode 是 APP 平時(shí)所處的狀態(tài),TrackingRunLoopMode 是追蹤 ScrollView 滑動時(shí)的狀態(tài)厢破。當(dāng)你創(chuàng)建一個(gè) Timer 并加到 DefaultMode 時(shí)荣瑟, Timer 會得到重復(fù)回調(diào),當(dāng)你滑動 TableView 時(shí)摩泪,Mode 就會切換成為 TrackingRunLoopMode ,這個(gè)時(shí)候 Timer 就不會回調(diào),同時(shí)也不會影響到滑動操作劫谅。

有時(shí)候你需要一個(gè) Timer在兩個(gè) Mode 中都可以得到回調(diào)见坑,方法一就是將這個(gè) Timer 分別加入到這兩個(gè) Mode;方法二就是將 Timer 加入到頂層的 RunLoop 的 "commonModeItems" 中捏检。

Mode 相關(guān)的接口

CFRunLoop 對外暴露管理 Mode 接口:

CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFString modeName);
CFRunLoopRunInMode(CFStringRef modeName,...);

Mode 對外暴露的管理 item 的接口:

CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);

我們只能通過 modeName 來操作內(nèi)部的 Mode荞驴,如果你傳入一個(gè)新的 modeName 。但是 RunLoop 內(nèi)部不存在時(shí)贯城,RunLoop 會自動創(chuàng)建一個(gè)對應(yīng)的 CFRunLoopModeRef熊楼;對于一個(gè) RunLoop 來說,其內(nèi)部的 mode 只能增加不能刪除能犯。

我們在上面提到了兩種預(yù)置的 Mode鲫骗,當(dāng)我們切換到對應(yīng)的 Mode 時(shí)犬耻,我們只需要傳入對應(yīng)的名稱即可。然而执泰,還存在
KCFRunLoopCommonModes(NSRunloopCommonModes),它是一種組合模式枕磁,在 iOS 默認(rèn)包含了NSDefaultRunLoopModeUITrackingRunLoopMode(注意:并不是說Runloop會運(yùn)行在 kCFRunLoopCommonModes 這種模式下,而是相當(dāng)于分別注冊了 NSDefaultRunLoopMode 和 UITrackingRunLoopMode术吝。當(dāng)然你也可以通過調(diào)用CFRunLoopAddCommonMode()方法將自定義Mode放到 kCFRunLoopCommonModes 組合)计济。

注意:
我們常常還會碰到一些系統(tǒng)框架自定義Mode,例如Foundation中NSConnectionReplyMode排苍。還有一些系統(tǒng)私有Mode沦寂,例如:GSEventReceiveRunLoopMode接受系統(tǒng)事件,UIInitializationRunLoopMode App啟動過程中初始化Mode淘衙。

系統(tǒng)默認(rèn)注冊了5個(gè)Mode:

  • kCFRunLoopDefaultMode: APP 默認(rèn)的Mode传藏,通常主線程是在這個(gè) Mode 下運(yùn)行。
  • UITrackingRunLoopMode: 界面跟蹤 Mode幔翰,用于 ScrollView 追蹤觸摸滑動漩氨,保證界面滑動不受其他 Mode 影響。
  • UIInitializationRunLoopMode: 在剛啟動 APP 時(shí)進(jìn)入的第一個(gè) Mode遗增,啟動完之后就不會在使用叫惊。
  • GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內(nèi)部 Mode,通常用不到。
  • kCFRunLoopCommonModes: 占位 Mode做修,沒有實(shí)際作用霍狰。

當(dāng) RunLoop 進(jìn)行回調(diào)時(shí),一般都是通過一個(gè)很長的函數(shù)調(diào)用出去(call out),當(dāng)你在你的代碼中斷點(diǎn)調(diào)試時(shí)饰及,通常能在調(diào)用棧上看到這些函數(shù)蔗坯。

RunLoop 的內(nèi)部邏輯

首先,通過一張圖燎含,來了解一下 RunLoop 的運(yùn)行流程:


RunLoop 的運(yùn)行流程

內(nèi)部代碼整理如下宾濒;

// 用DefaultMode啟動
void CFRunLoopRun(void) {
    CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}
 
// 用指定的Mode啟動,允許設(shè)置RunLoop超時(shí)時(shí)間
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
 
// RunLoop的實(shí)現(xiàn)
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
    
    // 首先根據(jù)modeName找到對應(yīng)mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
    // 如果mode里沒有source/timer/observer, 直接返回屏箍。
    if (__CFRunLoopModeIsEmpty(currentMode)) return;

    // 1. 通知 Observers: RunLoop 即將進(jìn)入 loop绘梦。
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
    
    // 內(nèi)部函數(shù),進(jìn)入loop
    __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
        
        Boolean sourceHandledThisLoop = NO;
        int retVal = 0;
        do {
 
            // 2. 通知 Observers: RunLoop 即將觸發(fā) Timer 回調(diào)赴魁。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
            // 3. 通知 Observers: RunLoop 即將觸發(fā) Source0 (非port) 回調(diào)卸奉。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
            // 執(zhí)行被加入的block
            __CFRunLoopDoBlocks(runloop, currentMode);
            
            // 4. RunLoop 觸發(fā) Source0 (非port) 回調(diào)。
            sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
            // 執(zhí)行被加入的block
            __CFRunLoopDoBlocks(runloop, currentMode);
 
            // 5. 如果有 Source1 (基于port) 處于 ready 狀態(tài)颖御,直接處理這個(gè) Source1 然后跳轉(zhuǎn)去處理消息榄棵。
            if (__Source0DidDispatchPortLastTime) {
                Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
                if (hasMsg) goto handle_msg;
            }
            
            // 通知 Observers: RunLoop 的線程即將進(jìn)入休眠(sleep)。
            if (!sourceHandledThisLoop) {
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
            }
            
            // 7. 調(diào)用 mach_msg 等待接受 mach_port 的消息。線程將進(jìn)入休眠, 直到被下面某一個(gè)事件喚醒疹鳄。
            // ? 一個(gè)基于 port 的Source 的事件拧略。
            // ? 一個(gè) Timer 到時(shí)間了
            // ? RunLoop 自身的超時(shí)時(shí)間到了
            // ? 被其他什么調(diào)用者手動喚醒
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
                mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
            }
 
            // 8. 通知 Observers: RunLoop 的線程剛剛被喚醒了。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
            
            // 收到消息尚辑,處理消息辑鲤。
            handle_msg:
 
            // 9.1 如果一個(gè) Timer 到時(shí)間了,觸發(fā)這個(gè)Timer的回調(diào)杠茬。
            if (msg_is_timer) {
                __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
            } 
 
            // 9.2 如果有dispatch到main_queue的block月褥,執(zhí)行block。
            else if (msg_is_dispatch) {
                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            } 
 
            // 9.3 如果一個(gè) Source1 (基于port) 發(fā)出事件了瓢喉,處理這個(gè)事件
            else {
                CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
                sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
                if (sourceHandledThisLoop) {
                    mach_msg(reply, MACH_SEND_MSG, reply);
                }
            }
            
            // 執(zhí)行加入到Loop的block
            __CFRunLoopDoBlocks(runloop, currentMode);
            
 
            if (sourceHandledThisLoop && stopAfterHandle) {
                // 進(jìn)入loop時(shí)參數(shù)說處理完事件就返回宁赤。
                retVal = kCFRunLoopRunHandledSource;
            } else if (timeout) {
                // 超出傳入?yún)?shù)標(biāo)記的超時(shí)時(shí)間了
                retVal = kCFRunLoopRunTimedOut;
            } else if (__CFRunLoopIsStopped(runloop)) {
                // 被外部調(diào)用者強(qiáng)制停止了
                retVal = kCFRunLoopRunStopped;
            } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
                // source/timer/observer一個(gè)都沒有了
                retVal = kCFRunLoopRunFinished;
            }
            
            // 如果沒超時(shí),mode里沒空栓票,loop也沒被停止决左,那繼續(xù)loop。
        } while (retVal == 0);
    }
    
    // 10. 通知 Observers: RunLoop 即將退出走贪。
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}

從源碼很容易看出佛猛,Runloop 總是運(yùn)行在某種特定的 CFRunLoopModeRef 下(每次運(yùn)行__CFRunLoopRun()函數(shù)時(shí)必須指定 Mode )。它就是一個(gè)帶有一個(gè) do-while 循環(huán)的一個(gè)函數(shù)坠狡。當(dāng)你調(diào)用 CFRunLoopRun() 時(shí)继找,線程就會一直停留在這個(gè)循環(huán)里,只有超時(shí)逃沿、被手動停止或者 item 為空時(shí)婴渡,該函數(shù)才會返回。

RunLoop 的底層實(shí)現(xiàn)

其實(shí)凯亮,對于 RunLoop 而言边臼,最核心的就是保證線程在沒有消息時(shí)休眠從而避免占用系統(tǒng)資源,有消息傳入時(shí)能夠及時(shí)喚醒假消。而這個(gè)機(jī)制完全依靠系統(tǒng)內(nèi)核來完成柠并。

從上一節(jié)的源碼中可以看到,RunLoop 的核心是基于 mach port 的富拗,其進(jìn)入休眠時(shí)調(diào)用的函數(shù)是mach_msg()堂鲤。RunLoop 調(diào)用這個(gè)函數(shù)去接受消息,如果沒有外部發(fā)來的 port消息媒峡,內(nèi)核會一直將線程置于等待狀態(tài)。

RunLoop 的應(yīng)用

定時(shí)器

開頭就提到的Timer Source 作為事件源葵擎,它的上層對應(yīng)的就是 NSTimer(CFRunLoopTimerRef)谅阿。NSTimer 定時(shí)器的觸發(fā)基于 RunLoop 運(yùn)行,使用 NSTimer 之前必須注冊到 RunLoop,但是 RunLoop 為了節(jié)省資源并不會在非常準(zhǔn)確的時(shí)間點(diǎn)調(diào)用定時(shí)器签餐,如果一個(gè)任務(wù)執(zhí)行時(shí)間較長寓涨,那么當(dāng)錯(cuò)過一個(gè)時(shí)間點(diǎn)后只能等待下一個(gè)時(shí)間點(diǎn)執(zhí)行,并不會延后執(zhí)行(NSTimer 提供了一個(gè) tolerance 屬性用于設(shè)置寬容度氯檐,可以通過設(shè)置此屬性來盡可能的使 NSTimer 準(zhǔn)確)戒良。

CADisplayLink 是一個(gè)執(zhí)行頻率(fps)和屏幕刷新相同的定時(shí)器,可以修改preferredFramesPerSecond改變刷新頻率冠摄;它也需要加入到RunLoop才能執(zhí)行糯崎。與NSTimer類似,CADisplayLink同樣是基于CFRunloopTimerRef實(shí)現(xiàn)河泳,底層使用mk_timer(可以比較加入到RunLoop前后RunLoop中timer的變化)沃呢。和NSTimer相比它精度更高(盡管NSTimer也可以修改精度),不過和NStimer類似的是如果遇到大任務(wù)它仍然存在丟幀現(xiàn)象拆挥。通常情況下CADisaplayLink用于構(gòu)建幀動畫薄霜,看起來相對更加流暢,而NSTimer則有更廣泛的用處纸兔。

AutoreleasePool

AutoreleasePool是另一個(gè)與RunLoop相關(guān)討論較多的話題惰瓜。其實(shí)從RunLoop源代碼分析,AutoreleasePool與RunLoop并沒有直接的關(guān)系汉矿,之所以將兩個(gè)話題放到一起討論最主要的原因是因?yàn)樵?APP 啟動后崎坊,蘋果在主線程 RunLoop 里注冊了兩個(gè) Observer 管理和維護(hù) AutorealeasePool,其回調(diào)都是 _wrapRunLoopWithAutoreleasePoolHandler()负甸。

第一個(gè) Observer 監(jiān)視的事件是 Entry(即將進(jìn)入 Loop), 其回調(diào)內(nèi)會調(diào)用_objc_autoreleasePoolPush() 創(chuàng)建自動釋放池流强。它的優(yōu)先級最高,保證創(chuàng)建釋放池發(fā)生在其他所有回調(diào)之前呻待。

第二個(gè) Observer 監(jiān)視了兩個(gè)事件: BeforeWaaiting(準(zhǔn)備進(jìn)入休眠)時(shí)調(diào)用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 釋放舊的池并創(chuàng)建新的池打月;Exit(即將退出Loop)時(shí)調(diào)用_objc_autoreleasePoolPop()來釋放自動釋放池。它的優(yōu)先級最低蚕捉,保證其釋放池子發(fā)生在其他所有回調(diào)之后奏篙。

主線程中的其他操作通常均在這個(gè) AutorelsePool 之內(nèi)(main函數(shù)),以盡可能減少內(nèi)存維護(hù)操作迫淹。

事件響應(yīng)

蘋果注冊了一個(gè) Source1(基于 mach port)用來接受系統(tǒng)事件秘通,其回調(diào)函數(shù)為__IOHIDEventSystemClientQueueCallback()

當(dāng)一個(gè)硬件事件(觸摸/鎖屏/搖晃等)發(fā)生后敛熬,首先由 IOKit.framework 生成一個(gè) IOHIDEvent 事件并由 SpringBoard 接收肺稀。

SpringBoard 只接收按鍵(鎖屏/靜音等),觸摸应民,加速话原,接近傳感器等幾種Event夕吻,隨后用 mach port 轉(zhuǎn)發(fā)給需要的 App 進(jìn)程。隨后蘋果注冊的那個(gè) Source1 就會觸發(fā)回調(diào)繁仁,并調(diào)用 _UIApplicationHandleEventQueue() 進(jìn)行應(yīng)用內(nèi)部的分發(fā)涉馅。

_UIApplicationHandleEventQueue() 會把 IOHIDEvent 處理并包裝成 UIEvent 進(jìn)行處理或分發(fā),其中包括識別 UIGesture/處理屏幕旋轉(zhuǎn)/發(fā)送給UIWindow等黄虱。通常事件比如 UIButton 點(diǎn)擊稚矿、 TouchesBegin/Move/End/Cancel 事件都是在這個(gè)回調(diào)中完成的。

手勢識別

當(dāng)上面的_UIApplicationHandleEventQueue() 識別了一個(gè)手勢時(shí)捻浦,其首先會調(diào)用 Cancel 將當(dāng)前的 touchesBegin/Move/End 系列回調(diào)打斷晤揣。隨后系統(tǒng)將對應(yīng)的 UIGestureRecognizer 標(biāo)記為待處理。

蘋果注冊了一個(gè) Observer 監(jiān)測 BeforeWaiting (Loop即將進(jìn)入休眠) 事件默勾,這個(gè)Observer的回調(diào)函數(shù)是 _UIGestureRecognizerUpdateObserver()碉渡,其內(nèi)部會獲取所有剛被標(biāo)記為待處理的 GestureRecognizer,并執(zhí)行 GestureRecognizer 的回調(diào)母剥。

當(dāng)有 UIGestureRecognizer 的變化(創(chuàng)建/銷毀/狀態(tài)改變)時(shí)滞诺,這個(gè)回調(diào)都會進(jìn)行相應(yīng)處理。

界面更新

打印App啟動之后的主線程 RunLoop 可以發(fā)現(xiàn)另外一個(gè)callout為_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv的Observer环疼,這個(gè)監(jiān)聽專門負(fù)責(zé)UI變化后的更新习霹,比如修改了frame、調(diào)整了UI層級(UIView/CALayer)或者手動設(shè)置了setNeedsDisplay/setNeedsLayout之后就會將這些操作提交到全局容器炫隶。

這個(gè) Observers 監(jiān)聽了主線程 RunLoop 的 BeforeWaiting(即將進(jìn)入休眠)和 Exit (即將退出 Loop)狀態(tài)淋叶,一旦進(jìn)入到這兩種狀態(tài)則會遍歷所有的 UI 更新并提交進(jìn)行實(shí)際繪制更新。

該函數(shù)內(nèi)部的調(diào)用棧大致如下:

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
    QuartzCore:CA::Transaction::observer_callback:
        CA::Transaction::commit();
            CA::Context::commit_transaction();
                CA::Layer::layout_and_display_if_needed();
                    CA::Layer::layout_if_needed();
                        [CALayer layoutSublayers];
                            [UIView layoutSubviews];
                    CA::Layer::display_if_needed();
                        [CALayer display];
                            [UIView drawRect];

通常情況下這種方式是完美的伪阶,因?yàn)槌讼到y(tǒng)的更新煞檩,還可以利用 setNeedsDisplay 等方法手動觸發(fā)下一次 RunLoop 運(yùn)行的更新。但是如果當(dāng)前正在執(zhí)行大量的邏輯運(yùn)算可能UI的更新就會比較卡栅贴,因此facebook推出了AsyncDisplayKit來解決這個(gè)問題斟湃。有關(guān) AsyncDiskplayKit ,后文會具體講到檐薯。

PerformSelecter

當(dāng)調(diào)用 NSObject 的 performSelecter: afterDelay:后凝赛,實(shí)際上其內(nèi)部會創(chuàng)建一個(gè) Timer 并添加到當(dāng)前線程的 RunLoop 中。如果當(dāng)前線程沒有 runloop 坛缕,該方法會隨之失效墓猎。

當(dāng)調(diào)用performSelector: onThread: 時(shí),會創(chuàng)建一個(gè) Timer 加到對應(yīng)的線程中去,同樣的赚楚,如果對應(yīng)的線程沒有 RunLoop 該方法也會失效毙沾。

關(guān)于 GCD

在 RunLoop 的源代碼中可以看到 GCD 的相關(guān)東西,但是它倆本質(zhì)是沒有直接關(guān)系宠页。
當(dāng)調(diào)用dispatch_async(dispatch_get_main_queue(), block)時(shí)搀军,libDispatch 會向主線程的 RunLoop 發(fā)送消息膨俐,RunLoop會被喚醒,并從消息中取得這個(gè) block罩句,并在回調(diào) __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() 里執(zhí)行這個(gè) block。但這個(gè)邏輯僅限于 dispatch 到主線程敛摘,dispatch 到其他線程仍然是由 libDispatch 處理的门烂。

關(guān)于網(wǎng)絡(luò)請求

iOS 中,關(guān)于網(wǎng)絡(luò)請求的接口自下而上有如下幾層:

  • CFSocket:最底層的接口兄淫,只負(fù)責(zé) socket 通信屯远。
  • CFNetwork: 基于 CFSocket 等接口的上層封裝。
  • NSURLConnection:基于 CFNetwork 的更高層封裝捕虽,提供面向?qū)ο蟮慕涌?/li>
  • NSURLSession 是 iOS7 中新增的接口慨丐,表面上是和 NSURLConnection 并列的,但底層仍然用到了 NSNRLConnection 的部分功能泄私。

NSURLConnection 的工作過程

通常使用 NSURLConnection 時(shí)房揭,你會傳入一個(gè) Delegate,當(dāng)調(diào)用了 [connection start] 后晌端,這個(gè) Delegate 就會不停的收到事件回調(diào)捅暴。實(shí)際上,start 這個(gè)函數(shù)的內(nèi)部會獲取 CurrentRunLoop咧纠,然后在其中的 DefaultMode 添加了4個(gè) Source0(需要手動觸發(fā)的 Source)蓬痒。

  • CFHTTPCookieStorage: 用于處理 cookie
  • CFMultiplexerSource: 負(fù)責(zé)各種 Delegate 回調(diào),并在回調(diào)中喚醒 Delegate 內(nèi)部的 RunLoop(通常是主線程)來執(zhí)行實(shí)際操作漆羔。

當(dāng)開始網(wǎng)絡(luò)傳輸時(shí)梧奢, NSURLConnection 會創(chuàng)建兩個(gè)新線程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 線程是處理底層 socket 連接的演痒。NSURLConnectionLoader 這個(gè)線程內(nèi)部會使用 RunLoop 來接收底層 sokcet 的事件亲轨,并通過之前添加的 Source0 通知到上層的 Delegate。


image

NSURLConnectionLoader 中的 RunLoop 通過一些基于 mach port 的 Source 接收來自底層 CFSocket 的通知嫡霞。當(dāng)收到通知后瓶埋,其會在合適的時(shí)機(jī)向 CFMultiplexerSource 等 Source0 發(fā)送通知,同時(shí)喚醒 Delegate 線程的 RunLoop 來讓其處理這些通知诊沪。CFMultiplexerSource 會在 Delegate 線程的 RunLoop 對 Delegate 執(zhí)行實(shí)際的回調(diào)养筒。

具體實(shí)例舉例

AFNetworking 2.x

AF 2.x 基于NSURLConnection包裝的重要對象,由于iOS9-NSURLConnection已經(jīng)不能使用端姚,AFNetworking在3.x版本中刪除了基于 NSURLConnection API的所有支持晕粪。

因此,我們要研究的就是 AFNetworking 2.x 渐裸。

AFURLConnectionOperation 這個(gè)類是基于 NSURLConnection 構(gòu)建的巫湘。AFNetworking 單獨(dú)創(chuàng)建了一個(gè)線程装悲,并在這個(gè)線程中啟動了一個(gè) RunLoop,在后臺接收 Delegate 回調(diào)。具體代碼如下:

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}
 
+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });
    return _networkRequestThread;
}

RunLoop 啟動前內(nèi)部必須要有至少一個(gè) Timer/Observer/Source尚氛,所以 AFNetworking 在 [runLoop run] 之前先創(chuàng)建了一個(gè)新的 NSMachport 添加進(jìn)去了诀诊。通常情況下,調(diào)用者需要持有這個(gè) NSMachPort(mach_port) 并在外部線程通過這個(gè) port 發(fā)送消息到 loop 內(nèi)阅嘶;此處添加的 port 只是為了讓 RunLoop 不退出属瓣。

- (void)start {
    [self.lock lock];
    if ([self isCancelled]) {
        [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    } else if ([self isReady]) {
        self.state = AFOperationExecutingState;
        [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    }
    [self.lock unlock];
}

當(dāng)需要這個(gè)后臺執(zhí)行任務(wù)時(shí), AFNetworking 通過調(diào)用 [NSObject performSelector: onThread:...] 將這個(gè)任務(wù)扔到了后臺線程的 RunLoop 中讯柔。

AsyncDisplayKit

AsyncDisplayKit 是 Facebook 推出的用于保持界面流暢性的框架抡蛙,其原理大致如下:

UI 線程中的任務(wù)通常分為三類:排版、繪制魂迄、UI 對象操作粗截,當(dāng)這些任務(wù)過于繁重的話就會導(dǎo)致界面卡頓。

排版通常包括計(jì)算視圖大小捣炬、計(jì)算文本高度熊昌、重新計(jì)算子視圖的排版等操作。

繪制一般有文本繪制(coreText)遥金、圖片繪制(例如預(yù)先解壓)浴捆、元素繪制(Quartz)等操作。

UI 對象操作通常包括 UIView/CALayer 等 UI 對象的創(chuàng)建稿械、設(shè)置屬性和銷毀选泻。

其中前兩類操作可以通過各種方法放到后臺去執(zhí)行,而最后一項(xiàng)操作只能在主線程完成美莫,并且有時(shí)后面的操作需要依賴前面操作的結(jié)果(TextView 創(chuàng)建時(shí)可能需要提前計(jì)算出文本的大幸趁小)。

AsyncDisplayKit 所做的就是盡量將能放入到后臺的任務(wù)放入后臺厢呵,不能的則盡量推遲(例如視圖的創(chuàng)建窝撵、屬性的調(diào)整)。因此襟铭, AsyncDisplayKit 創(chuàng)建了一個(gè)名為 AsyDisplayNode 的對象碌奉,并在其內(nèi)部封裝了 UIView/CALayer,它具有和 UIView/CALayer 相似的屬性寒砖,例如frame赐劣、backgroundColor 等。所有這些屬性都可以在后臺線程更改哩都,開發(fā)者只可以通過 Node 來操作其內(nèi)部的 UIView/CALayer .魁兼,這些就可以將排版和繪制放入了后臺線程。但是無論怎樣操作漠嵌,但是屬性總需要在某個(gè)時(shí)刻同步到主線程的 UIView/CALayer 去咐汞。

AsyncDisplayKit 仿照 QuartzCore/UIKit 框架的模式盖呼,實(shí)現(xiàn)了一套類似的界面更新的機(jī)制:即在主線程的 RunLoop 中添加一個(gè) Observer,監(jiān)聽了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件化撕,在收到回調(diào)時(shí)几晤,遍歷所有之前放入隊(duì)列的待處理的任務(wù),然后一一執(zhí)行侯谁。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末锌仅,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子墙贱,更是在濱河造成了極大的恐慌,老刑警劉巖贱傀,帶你破解...
    沈念sama閱讀 218,386評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件惨撇,死亡現(xiàn)場離奇詭異,居然都是意外死亡府寒,警方通過查閱死者的電腦和手機(jī)魁衙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來株搔,“玉大人剖淀,你說我怎么就攤上這事∠朔浚” “怎么了纵隔?”我有些...
    開封第一講書人閱讀 164,704評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長炮姨。 經(jīng)常有香客問我捌刮,道長,這世上最難降的妖魔是什么舒岸? 我笑而不...
    開封第一講書人閱讀 58,702評論 1 294
  • 正文 為了忘掉前任绅作,我火速辦了婚禮,結(jié)果婚禮上蛾派,老公的妹妹穿的比我還像新娘俄认。我一直安慰自己,他們只是感情好洪乍,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,716評論 6 392
  • 文/花漫 我一把揭開白布眯杏。 她就那樣靜靜地躺著,像睡著了一般典尾。 火紅的嫁衣襯著肌膚如雪役拴。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,573評論 1 305
  • 那天钾埂,我揣著相機(jī)與錄音河闰,去河邊找鬼科平。 笑死,一個(gè)胖子當(dāng)著我的面吹牛姜性,可吹牛的內(nèi)容都是我干的瞪慧。 我是一名探鬼主播,決...
    沈念sama閱讀 40,314評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼部念,長吁一口氣:“原來是場噩夢啊……” “哼弃酌!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起儡炼,我...
    開封第一講書人閱讀 39,230評論 0 276
  • 序言:老撾萬榮一對情侶失蹤妓湘,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后乌询,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體榜贴,經(jīng)...
    沈念sama閱讀 45,680評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,873評論 3 336
  • 正文 我和宋清朗相戀三年妹田,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了唬党。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,991評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡鬼佣,死狀恐怖驶拱,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情晶衷,我是刑警寧澤遏餐,帶...
    沈念sama閱讀 35,706評論 5 346
  • 正文 年R本政府宣布狭园,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏闽巩。R本人自食惡果不足惜忠藤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,329評論 3 330
  • 文/蒙蒙 一珍手、第九天 我趴在偏房一處隱蔽的房頂上張望准夷。 院中可真熱鬧,春花似錦凌蔬、人聲如沸露懒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽懈词。三九已至,卻和暖如春辩诞,著一層夾襖步出監(jiān)牢的瞬間坎弯,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留抠忘,地道東北人撩炊。 一個(gè)月前我還...
    沈念sama閱讀 48,158評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像崎脉,于是被迫代替她去往敵國和親拧咳。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,941評論 2 355

推薦閱讀更多精彩內(nèi)容