深入理解RunLoop

最近看了很多RunLoop的文章,看完很懵逼,決心整理一下,文章中大部分內(nèi)容都是引用大神們的,但好歹對(duì)自己有個(gè)交代了,花了一個(gè)周天加幾個(gè)晚上熬夜完成的,有個(gè)產(chǎn)出還是很爽的,不多比比了,下面開始吧。

什么是RunLoop?

RunLoop是一個(gè)接收處理異步消息事件的循環(huán),一個(gè)循環(huán)中:等待事件發(fā)生,然后將這個(gè)事件送到能處理它的地方。

RunLoop實(shí)際上是一個(gè)對(duì)象,這個(gè)對(duì)象在循環(huán)中用來處理程序運(yùn)行過程中出現(xiàn)的各種事件(比如說觸摸事件、UI刷新事件、定時(shí)器事件、Selector事件)和消息,從而保持程序的持續(xù)運(yùn)行;而且在沒有事件處理的時(shí)候,會(huì)進(jìn)入睡眠模式,從而節(jié)省CPU資源,提高程序性能。

Event Loop模型偽代碼

int main(int argc, char * argv[]) {    
     //程序一直運(yùn)行狀態(tài)
     while (AppIsRunning) {
          //睡眠狀態(tài),等待喚醒事件
          id whoWakesMe = SleepForWakingU  p();
          //得到喚醒事件
          id event = GetEvent(whoWakesMe);
          //開始處理事件
          HandleEvent(event);
     }
     return 0;
}
Screen Shot 2018-03-25 at 3.41.32 PM.png
  • mach kernel屬于蘋果內(nèi)核,RunLoop依靠它實(shí)現(xiàn)了休眠和喚醒而避免了CPU的空轉(zhuǎn)。
  • Runloop是基于pthread進(jìn)行管理的,pthread是基于c的跨平臺(tái)多線程操作底層API。它是mach thread的上層封裝(可以參見Kernel Programming Guide),和NSThread一一對(duì)應(yīng)(而NSThread是一套面向?qū)ο蟮腁PI,所以在iOS開發(fā)中我們也幾乎不用直接使用pthread)。
Screen Shot 2018-03-25 at 4.28.26 PM.png

RunLoop的組成

RunLoop構(gòu)成

CFRunLoop對(duì)象可以檢測(cè)某個(gè)task或者dispatch的輸入事件,當(dāng)檢測(cè)到有輸入源事件,CFRunLoop將會(huì)將其加入到線程中進(jìn)行處理。比方說用戶輸入事件、網(wǎng)絡(luò)連接事件、周期性或者延時(shí)事件、異步的回調(diào)等。

RunLoop可以檢測(cè)的事件類型一共有3種,分別是CFRunLoopSource、CFRunLoopTimer、CFRunLoopObserver??梢酝ㄟ^CFRunLoopAddSource, CFRunLoopAddTimer或者CFRunLoopAddObserver添加相應(yīng)的事件類型。

要讓一個(gè)RunLoop跑起來還需要run loop modes,每一個(gè)source, timer和observer添加到RunLoop中時(shí)必須要與一個(gè)模式(CFRunLoopMode)相關(guān)聯(lián)才可以運(yùn)行。

上面是對(duì)于CFRunLoop官方文檔的解釋

RunLoop的主要組成
RunLoop共包含5個(gè)類,但公開的只有Source、Timer、Observer相關(guān)的三個(gè)類。

CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef

image

CFRunLoopSourceRef
source是RunLoop的數(shù)據(jù)源(輸入源)的抽象類(protocol),Source有兩個(gè)版本:Source0 和 Source1

  • source0:只包含了一個(gè)回調(diào)(函數(shù)指針),使用時(shí),你需要先調(diào)用 CFRunLoopSourceSignal(source),將這個(gè) Source 標(biāo)記為待處理,然后手動(dòng)調(diào)用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個(gè)事件。處理App內(nèi)部事件,App自己負(fù)責(zé)管理(出發(fā)),如UIEvent(Touch事件等,GS發(fā)起到RunLoop運(yùn)行再到事件回調(diào)到UI)、CFSocketRef。
  • Source1:由RunLoop和內(nèi)核管理,由mach_port驅(qū)動(dòng)(特指port-based事件),如CFMachPort、CFMessagePort、NSSocketPort。特別要注意一下Mach port的概念,它是一個(gè)輕量級(jí)的進(jìn)程間通訊的方式,可以理解為它是一個(gè)通訊通道,假如同時(shí)有幾個(gè)進(jìn)程都掛在這個(gè)通道上,那么其它進(jìn)程向這個(gè)通道發(fā)送消息后,這些掛在這個(gè)通道上的進(jìn)程都可以收到相應(yīng)的消息。這個(gè)Port的概念非常重要,因?yàn)樗荝unLoop休眠和被喚醒的關(guān)鍵,它是RunLoop與系統(tǒng)內(nèi)核進(jìn)行消息通訊的窗口。

CFRunLoopTimerRef 是基于時(shí)間的觸發(fā)器,它和 NSTimer 是toll-free bridged 的,可以混用(底層基于使用mk_timer實(shí)現(xiàn))。它受RunLoop的Mode影響(GCD的定時(shí)器不受RunLoop的Mode影響),當(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)。如果線程阻塞或者不在這個(gè)Mode下,觸發(fā)點(diǎn)將不會(huì)執(zhí)行,一直等到下一個(gè)周期時(shí)間點(diǎn)觸發(fā)。

CFRunLoopObserverRef 是觀察者,每個(gè) Observer 都包含了一個(gè)回調(diào)(函數(shù)指針),當(dāng) RunLoop 的狀態(tài)發(fā)生變化時(shí),觀察者就能通過回調(diào)接受到這個(gè)變化??梢杂^測(cè)的時(shí)間點(diǎn)有以下幾個(gè)

enum CFRunLoopActivity {
    kCFRunLoopEntry              = (1 << 0),    // 即將進(jìn)入Loop   
    kCFRunLoopBeforeTimers      = (1 << 1),    // 即將處理 Timer        
    kCFRunLoopBeforeSources     = (1 << 2),    // 即將處理 Source  
    kCFRunLoopBeforeWaiting     = (1 << 5),    // 即將進(jìn)入休眠     
    kCFRunLoopAfterWaiting      = (1 << 6),    // 剛從休眠中喚醒   
    kCFRunLoopExit               = (1 << 7),    // 即將退出Loop  
    kCFRunLoopAllActivities     = 0x0FFFFFFFU  // 包含上面所有狀態(tài)  
};
typedef enum CFRunLoopActivity CFRunLoopActivity;

這里要提一句的是,timer和source1(也就是基于port的source)可以反復(fù)使用,比如timer設(shè)置為repeat,port可以持續(xù)接收消息,而source0在一次觸發(fā)后就會(huì)被runloop移除。

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

RunLoop主要處理以下6類事件

static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();

RunLoop的Mode

CFRunLoopMode 和 CFRunLoop的結(jié)構(gòu)大致如下:

struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
    CFMutableSetRef _sources0;    // Set
    CFMutableSetRef _sources1;    // Set
    CFMutableArrayRef _observers; // Array
    CFMutableArrayRef _timers;    // Array
    ...
};

struct __CFRunLoop {
    CFMutableSetRef _commonModes;     // Set
    CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
    CFRunLoopModeRef _currentMode;    // Current Runloop Mode
    CFMutableSetRef _modes;           // Set
    ...
}; 

一個(gè)RunLoop包含了多個(gè)Mode,每個(gè)Mode又包含了若干個(gè)Source/Timer/Observer。每次調(diào)用 RunLoop的主函數(shù)時(shí),只能指定其中一個(gè)Mode,這個(gè)Mode被稱作CurrentMode。如果需要切換 Mode,只能退出Loop,再重新指定一個(gè)Mode進(jìn)入。這樣做主要是為了分隔開不同Mode中的Source/Timer/Observer,讓其互不影響。下面是5種Mode

  • kCFDefaultRunLoopMode App的默認(rèn)Mode,通常主線程是在這個(gè)Mode下運(yùn)行
  • UITrackingRunLoopMode 界面跟蹤Mode,用于ScrollView追蹤觸摸滑動(dòng),保證界面滑動(dòng)時(shí)不受其他Mode影響
  • UIInitializationRunLoopMode 在剛啟動(dòng)App時(shí)第進(jìn)入的第一個(gè)Mode,啟動(dòng)完成后就不再使用
  • GSEventReceiveRunLoopMode 接受系統(tǒng)事件的內(nèi)部Mode,通常用不到
  • kCFRunLoopCommonModes 這是一個(gè)占位用的Mode,不是一種真正的Mode

其中kCFDefaultRunLoopMode、UITrackingRunLoopMode是蘋果公開的,其余的mode都是無法添加的。那為何我們又可以這么用呢

[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

什么是CommonModes?

一個(gè) Mode 可以將自己標(biāo)記為”Common”屬性(通過將其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每當(dāng) RunLoop 的內(nèi)容發(fā)生變化時(shí),RunLoop 都會(huì)自動(dòng)將 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 標(biāo)記的所有Mode里
主線程的 RunLoop 里有 kCFRunLoopDefaultMode 和 UITrackingRunLoopMode,這兩個(gè)Mode都已經(jīng)被標(biāo)記為”Common”屬性。當(dāng)你創(chuàng)建一個(gè)Timer并加到DefaultMode時(shí),Timer會(huì)得到重復(fù)回調(diào),但此時(shí)滑動(dòng)一個(gè) scrollView 時(shí),RunLoop 會(huì)將 mode 切換為TrackingRunLoopMode,這時(shí)Timer就不會(huì)被回調(diào),并且也不會(huì)影響到滑動(dòng)操作。
如果想讓scrollView滑動(dòng)時(shí)Timer可以正常調(diào)用,一種辦法就是手動(dòng)將這個(gè) Timer 分別加入這兩個(gè) Mode。另一種方法就是將 Timer 加入到CommonMode 中。

怎么將事件加入到CommonMode?

我們調(diào)用上面的代碼將 Timer 加入到CommonMode 時(shí),但實(shí)際并沒有 CommonMode,其實(shí)系統(tǒng)將這個(gè) Timer 加入到頂層的 RunLoop 的 commonModeItems 中。commonModeItems 會(huì)被 RunLoop 自動(dòng)更新到所有具有”Common”屬性的 Mode 里去。
這一步其實(shí)是系統(tǒng)幫我們將Timer加到了kCFRunLoopDefaultMode和UITrackingRunLoopMode中。

在項(xiàng)目中最常用的就是設(shè)置NSTimer的Mode,比較簡(jiǎn)單這里就不說了。

RunLoop運(yùn)行機(jī)制

image

當(dāng)你調(diào)用 CFRunLoopRun() 時(shí),線程就會(huì)一直停留在這個(gè)循環(huán)里;直到超時(shí)或被手動(dòng)停止,該函數(shù)才會(huì)返回。每次線程運(yùn)行RunLoop都會(huì)自動(dòng)處理之前未處理的消息,并且將消息發(fā)送給觀察者,讓事件得到執(zhí)行。RunLoop運(yùn)行時(shí)首先根據(jù)modeName找到對(duì)應(yīng)mode,如果mode里沒有source/timer/observer,直接返回。

/// 用DefaultMode啟動(dòng)
void CFRunLoopRun(void) {
    CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}
 
/// 用指定的Mode啟動(dòng),允許設(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找到對(duì)應(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)用者手動(dòng)喚醒
            __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的掛起和喚醒

RunLoop的掛起

RunLoop的掛起是通過_CFRunLoopServiceMachPort —call—> mach_msg —call—> mach_msg_trap這個(gè)調(diào)用順序來告訴內(nèi)核RunLoop監(jiān)聽哪個(gè)mach_port(上面提到的消息通道),然后等待事件的發(fā)生(等待與InputSource、Timer描述內(nèi)容相關(guān)的事件),這樣內(nèi)核就把RunLoop掛起了,即RunLoop休眠了。

RunLoop的喚醒

這接種情況下會(huì)被喚醒

  1. 存在Source0被標(biāo)記為待處理,系統(tǒng)調(diào)用CFRunLoopWakeUp喚醒線程處理事件
  2. 定時(shí)器時(shí)間到了
  3. RunLoop自身的超時(shí)時(shí)間到了
  4. RunLoop外部調(diào)用者喚醒

當(dāng)RunLoop被掛起后,如果之前監(jiān)聽的事件發(fā)生了,由另一個(gè)線程(或另一個(gè)進(jìn)程中的某個(gè)線程)向內(nèi)核發(fā)送這個(gè)mach_port的msg后,trap狀態(tài)被喚醒,RunLoop繼續(xù)運(yùn)行

處理事件

  1. 如果一個(gè) Timer 到時(shí)間了,觸發(fā)這個(gè)Timer的回調(diào)
  2. 如果有dispatch到main_queue的block,執(zhí)行block
  3. 如果一個(gè) Source1 發(fā)出事件了,處理這個(gè)事件

事件處理完成進(jìn)行判斷

  1. 進(jìn)入loop時(shí)傳入?yún)?shù)指明處理完事件就返回(stopAfterHandle)
  2. 超出傳入?yún)?shù)標(biāo)記的超時(shí)時(shí)間(timeout)
  3. 被外部調(diào)用者強(qiáng)制停止__CFRunLoopIsStopped(runloop)
  4. source/timer/observer 全都空了__CFRunLoopModeIsEmpty(runloop, currentMode)

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

關(guān)于這個(gè)大家可以看ibireme的深入理解RunLoop一文,我這里選擇一些覺得比較重要又不是那么難懂的。
Mach消息發(fā)送機(jī)制看這篇文章Mach消息發(fā)送機(jī)制

為了實(shí)現(xiàn)消息的發(fā)送和接收,mach_msg() 函數(shù)實(shí)際上是調(diào)用了一個(gè) Mach 陷阱 (trap),即函數(shù)mach_msg_trap(),陷阱這個(gè)概念在 Mach 中等同于系統(tǒng)調(diào)用。當(dāng)你在用戶態(tài)調(diào)用 mach_msg_trap() 時(shí)會(huì)觸發(fā)陷阱機(jī)制,切換到內(nèi)核態(tài);內(nèi)核態(tài)中內(nèi)核實(shí)現(xiàn)的 mach_msg() 函數(shù)會(huì)完成實(shí)際的工作,如下圖:

image

RunLoop 的核心就是一個(gè) mach_msg() (見上面代碼的第7步),RunLoop 調(diào)用這個(gè)函數(shù)去接收消息,如果沒有別人發(fā)送 port 消息過來,內(nèi)核會(huì)將線程置于等待狀態(tài)。例如你在模擬器里跑起一個(gè) iOS 的 App,然后在 App 靜止時(shí)點(diǎn)擊暫停,你會(huì)看到主線程調(diào)用棧是停留在 mach_msg_trap() 這個(gè)地方。

RunLoop和線程

RunLoop和線程是息息相關(guān)的,我們知道線程的作用是用來執(zhí)行特定的一個(gè)或多個(gè)任務(wù),但是在默認(rèn)情況下,線程執(zhí)行完之后就會(huì)退出,就不能再執(zhí)行任務(wù)了。這時(shí)我們就需要采用一種方式來讓線程能夠處理任務(wù),并不退出。所以,我們就有了RunLoop。

iOS開發(fā)中能遇到兩個(gè)線程對(duì)象: pthread_t和NSThread,pthread_t和NSThread 是一一對(duì)應(yīng)的。比如,你可以通過 pthread_main_thread_np()或 [NSThread mainThread]來獲取主線程;也可以通過pthread_self()或[NSThread currentThread]來獲取當(dāng)前線程。CFRunLoop 是基于 pthread 來管理的。

線程與RunLoop是一一對(duì)應(yīng)的關(guān)系(對(duì)應(yīng)關(guān)系保存在一個(gè)全局的Dictionary里),線程創(chuàng)建之后是沒有RunLoop的(主線程除外),RunLoop的創(chuàng)建是發(fā)生在第一次獲取時(shí),銷毀則是在線程結(jié)束的時(shí)候。只能在當(dāng)前線程中操作當(dāng)前線程的RunLoop,而不能去操作其他線程的RunLoop。

蘋果不允許直接創(chuàng)建RunLoop,但是可以通過[NSRunLoop currentRunLoop]或者CFRunLoopGetCurrent()來獲?。ㄈ绻麤]有就會(huì)自動(dòng)創(chuàng)建一個(gè))。


/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 訪問 loopsDic 時(shí)的鎖
static CFSpinLock_t loopsLock;
 
/// 獲取一個(gè) pthread 對(duì)應(yīng)的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
    OSSpinLockLock(&loopsLock);
    
    if (!loopsDic) {
        // 第一次進(jìn)入時(shí),初始化全局Dic,并先為主線程創(chuàng)建一個(gè) RunLoop。
        loopsDic = CFDictionaryCreateMutable();
        CFRunLoopRef mainLoop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
    }
    
    /// 直接從 Dictionary 里獲取。
    CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
    
    if (!loop) {
        /// 取不到時(shí),創(chuàng)建一個(gè)
        loop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, thread, loop);
        /// 注冊(cè)一個(gè)回調(diào),當(dāng)線程銷毀時(shí),順便也銷毀其對(duì)應(yīng)的 RunLoop。
        _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
    }
    
    OSSpinLockUnLock(&loopsLock);
    return loop;
}
 
CFRunLoopRef CFRunLoopGetMain() {
    return _CFRunLoopGet(pthread_main_thread_np());
}
 
CFRunLoopRef CFRunLoopGetCurrent() {
    return _CFRunLoopGet(pthread_self());
}

開發(fā)過程中需要RunLoop時(shí),則需要手動(dòng)創(chuàng)建和運(yùn)行RunLoop(尤其是在子線程中, 主線程中的Main RunLoop除外),我看到別人舉了這么個(gè)例子,很有意思

調(diào)用[NSTimer scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:]帶有schedule的方法簇來啟動(dòng)Timer.

此方法會(huì)創(chuàng)建Timer并把Timer放到當(dāng)前線程的RunLoop中,隨后RunLoop會(huì)在Timer設(shè)定的時(shí)間點(diǎn)回調(diào)Timer綁定的selector或Invocation。但是,在主線程和子線程中調(diào)用此方法的效果是有差異的,即在主線程中調(diào)用scheduledTimer方法時(shí)timer可以在設(shè)定的時(shí)間點(diǎn)觸發(fā),但是在子線程里則不能觸發(fā)。這是因?yàn)樽泳€程中沒有創(chuàng)建RunLoop且更沒有啟動(dòng)RunLoop,而主線程中的RunLoop默認(rèn)是創(chuàng)建好的且一直運(yùn)行著。所以,子線程中需要像下面這樣調(diào)用。

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doTimer) userInfo:nil repeats:NO];
  [[NSRunLoop currentRunLoop] run];
});

那為什么下面這樣調(diào)用同樣不會(huì)觸發(fā)Timer呢?
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  [[NSRunLoop currentRunLoop] run];
  [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doTimer) userInfo:nil repeats:NO];
});  

我的分析是:scheduledTimerWithTimeInterval內(nèi)部在向RunLoop傳遞Timer時(shí)是調(diào)用與線程實(shí)例相關(guān)的單例方法[NSRunLoop currentRunLoop]來獲取RunLoop實(shí)例的,即RunLoop實(shí)例不存在就創(chuàng)建一個(gè)與當(dāng)前線程相關(guān)的RunLoop并把Timer傳遞到RunLoop中,存在則直接傳Timer到RunLoop中即可。而在RunLoop開始運(yùn)行后再向其傳遞Timer時(shí),由于dispatch_async代碼塊里的兩行代碼是順序執(zhí)行,[[NSRunLoop currentRunLoop] run]是一個(gè)沒有結(jié)束時(shí)間的RunLoop,無法執(zhí)行到“[NSTimer scheduledTimerWithTimeInterval:…”這一行代碼,Timer也就沒有被加到當(dāng)前RunLoop中,所以更不會(huì)觸發(fā)Timer了。

蘋果用 RunLoop 實(shí)現(xiàn)的功能

AutoreleasePool

App啟動(dòng)之后,系統(tǒng)啟動(dòng)主線程并創(chuàng)建了RunLoop,在main thread中注冊(cè)了兩個(gè)observer,回調(diào)都是_wrapRunLoopWithAutoreleasePoolHandler()

第一個(gè)Observer監(jiān)視的事件

  1. 即將進(jìn)入Loop(kCFRunLoopEntry),其回調(diào)內(nèi)會(huì)調(diào)用 _objc_autoreleasePoolPush() 創(chuàng)建自動(dòng)釋放池。其order是-2147483647,優(yōu)先級(jí)最高,保證創(chuàng)建釋放池發(fā)生在其他所有回調(diào)之前。

第二個(gè)Observer監(jiān)視了兩個(gè)事件

  1. 準(zhǔn)備進(jìn)入休眠(kCFRunLoopBeforeWaiting),此時(shí)調(diào)用 _objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 來釋放舊的池并創(chuàng)建新的池。

  2. 即將退出Loop(kCFRunLoopExit)此時(shí)調(diào)用 _objc_autoreleasePoolPop()釋放自動(dòng)釋放池。這個(gè) Observer的order是2147483647,確保池子釋放在所有回調(diào)之后。

我們知道AutoRelease對(duì)象是被AutoReleasePool管理的,那么AutoRelease對(duì)象在什么時(shí)候被回收呢?

第一種情況:在我們自己寫的for循環(huán)或線程體里,我們都習(xí)慣用AutoReleasePool來管理一些臨時(shí)變量的autorelease,使得在for循環(huán)或線程結(jié)束后回收AutoReleasePool的時(shí)候來回收AutoRelease臨時(shí)變量。

另一種情況:我們?cè)谥骶€程里創(chuàng)建了一些AutoRelease對(duì)象,這些對(duì)象可不能指望在回收Main AutoReleasePool時(shí)才被回收,因?yàn)锳pp一直運(yùn)行的過程中Main AutoReleasePool是不會(huì)被回收的。那么這種AutoRelease對(duì)象的回收就依賴Main RunLoop的運(yùn)行狀態(tài),Main RunLoop的Observer會(huì)在Main RunLoop結(jié)束休眠被喚醒時(shí)(kCFRunLoopAfterWaiting狀態(tài))通知UIKit,UIKit收到這一通知后就會(huì)調(diào)用_CFAutorleasePoolPop方法來回收主線程中的所有AutoRelease對(duì)象。

在主線程中執(zhí)行代碼一般都是寫在事件回調(diào)或Timer回調(diào)中的,這些回調(diào)都被加入了main thread的自動(dòng)釋放池中,所以在ARC模式下我們不用關(guān)心對(duì)象什么時(shí)候釋放,也不用去創(chuàng)建和管理pool。(如果事件不在主線程中要注意創(chuàng)建自動(dòng)釋放池,否則可能會(huì)出現(xiàn)內(nèi)存泄漏)。

NSTimer(timer觸發(fā))

上文說到了CFRunLoopTimerRef,其實(shí)NSTimer的原型就是CFRunLoopTimerRef。一個(gè)Timer注冊(cè) RunLoop 之后,RunLoop 會(huì)為這個(gè)Timer的重復(fù)時(shí)間點(diǎn)注冊(cè)好事件。有兩點(diǎn)需要注意:

  1. 但是需要注意的是RunLoop為了節(jié)省資源,并不會(huì)在非常準(zhǔn)確的時(shí)間點(diǎn)回調(diào)這個(gè)Timer。Timer 有個(gè)屬性叫做 Tolerance (寬容度),標(biāo)示了當(dāng)時(shí)間點(diǎn)到后,容許有多少最大誤差。這個(gè)誤差默認(rèn)為0,我們可以手動(dòng)設(shè)置這個(gè)誤差。文檔最后還強(qiáng)調(diào)了,為了防止時(shí)間點(diǎn)偏移,系統(tǒng)有權(quán)力給這個(gè)屬性設(shè)置一個(gè)值無論你設(shè)置的值是多少,即使RunLoop 模式正確,當(dāng)前線程并不阻塞,系統(tǒng)依然可能會(huì)在 NSTimer 上加上很小的的容差。
  2. 我們?cè)谀膫€(gè)線程調(diào)用 NSTimer 就必須在哪個(gè)線程終止

在RunLoop的Mode中也有說到,NSTimer使用的時(shí)候注意Mode,比如我之前開發(fā)時(shí)候用NSTimer寫一個(gè)Banner圖片輪播框架,如果不設(shè)置Timer的Mode為commonModes那么在滑動(dòng)TableView的時(shí)候Banner就停止輪播

DispatchQueue.global().async {
    // 非主線程不能使用 Timer.scheduledTimer進(jìn)行初始化
//                    self.timer = Timer.scheduledTimer(timeInterval: 6.0, target: self, selector: #selector(TurnPlayerView.didTurnPlay), userInfo: nil, repeats: false)
    
    if #available(iOS 10.0, *) {
        self.timer = Timer(timeInterval: 6.0, repeats: true, block: { (timer) in
            self.setContentOffset(CGPoint(x: self.frame.width*2, y: self.contentOffset.y), animated: true)
        })
    } else {
        // Fallback on earlier versions
    }
    
    
    RunLoop.main.add(self.timer!, forMode: RunLoopMode.commonModes)
}

和GCD的關(guān)系

  1. RunLoop底層用到GCD
  2. RunLoop與GCD并沒有直接關(guān)系,但當(dāng)GCD使用到main_queue時(shí)才有關(guān)系,如下:
//實(shí)驗(yàn)GCD Timer 與 Runloop的關(guān)系,只有當(dāng)dispatch_get_main_queue時(shí)才與RunLoop有關(guān)系
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    NSLog(@"GCD Timer...");
});

當(dāng)調(diào)用 dispatch_async(dispatch_get_main_queue(), block) 時(shí),libDispatch 會(huì)向主線程的 RunLoop 發(fā)送消息,RunLoop會(huì)被喚醒,并從消息中取得這個(gè) block,并在回調(diào) CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里執(zhí)行這個(gè) block。但這個(gè)邏輯僅限于 dispatch 到主線程,dispatch 到其他線程仍然是由 libDispatch 處理的。同理,GCD的dispatch_after在dispatch到main_queue時(shí)的timer機(jī)制才與RunLoop相關(guān)。

PerformSelecter

NSObject的performSelecter:afterDelay: 實(shí)際上其內(nèi)部會(huì)創(chuàng)建一個(gè) Timer 并添加到當(dāng)前線程的 RunLoop 中。所以如果當(dāng)前線程沒有 RunLoop,則這個(gè)方法會(huì)失效。
NSObject的performSelector:onThread: 實(shí)際上其會(huì)創(chuàng)建一個(gè) Timer 加到對(duì)應(yīng)的線程去,同樣的,如果對(duì)應(yīng)線程沒有 RunLoop 該方法也會(huì)失效。
其實(shí)這種方式有種說法也叫創(chuàng)建常駐線程(內(nèi)存),AFNetworking也用到這種技法。舉個(gè)例子,如果把RunLoop去掉,那么test方法就不會(huì)執(zhí)行。


class SecondViewController: UIViewController {
    
    var thread: Thread!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = UIColor.red
        thread = Thread.init(target: self, selector: #selector(SecondViewController.run), object: nil)
        thread.start()
        
    }
    
    @objc func run() {
        
        print("run -- ")
        RunLoop.current.add(Port(), forMode: .defaultRunLoopMode)
        RunLoop.current.run()
    }
    
    @objc func test() {
        print("test --  \(Thread.current)")
    }
    
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        //        self.test()
        
        self.perform(#selector(SecondViewController.test), on: thread, with: nil, waitUntilDone: false)
        
    }
    
    
}

網(wǎng)絡(luò)請(qǐng)求

iOS中的網(wǎng)絡(luò)請(qǐng)求接口自下而上有這么幾層

Screen Shot 2018-03-26 at 2.13.00 PM.png

其中CFSocket和CFNetwork偏底層,早些時(shí)候比較知名的網(wǎng)絡(luò)框架AFNetworking是基于NSURLConnection編寫的,iOS7之后新增了NSURLSession,NSURLSession的底層仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 線程),之后AFNetworking和Alamofire就是基于它封裝的了。

image

通常使用 NSURLConnection 時(shí),會(huì)傳入一個(gè) Delegate,當(dāng)調(diào)用了 [connection start] 后,這個(gè) Delegate 就會(huì)不停收到事件回調(diào)。實(shí)際上,start 這個(gè)函數(shù)的內(nèi)部會(huì)獲取 CurrentRunLoop,然后在其中的 DefaultMode 添加了4個(gè) Source0 (即需要手動(dòng)觸發(fā)的Source)。CFMultiplexerSource 是負(fù)責(zé)各種 Delegate 回調(diào)的,CFHTTPCookieStorage 是處理各種 Cookie 的。

開始網(wǎng)絡(luò)傳輸時(shí),NSURLConnection 創(chuàng)建了兩個(gè)新線程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。

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

事件響應(yīng)

蘋果注冊(cè)了一個(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)程。

觸摸事件其實(shí)是Source1接收系統(tǒng)事件后在回調(diào) __IOHIDEventSystemClientQueueCallback()內(nèi)觸發(fā)的 Source0,Source0 再觸發(fā)的 _UIApplicationHandleEventQueue()。source0一定是要喚醒runloop及時(shí)響應(yīng)并執(zhí)行的,如果runloop此時(shí)在休眠等待系統(tǒng)的 mach_msg事件,那么就會(huì)通過source1來喚醒runloop執(zhí)行。

_UIApplicationHandleEventQueue() 會(huì)把 IOHIDEvent 處理并包裝成 UIEvent 進(jìn)行處理或分發(fā),其中包括識(shí)別 UIGesture/處理屏幕旋轉(zhuǎn)/發(fā)送給 UIWindow 等。

image

手勢(shì)識(shí)別

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

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

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

UI更新

Core Animation 在 RunLoop 中注冊(cè)了一個(gè) Observer 監(jiān)聽 BeforeWaiting(即將進(jìn)入休眠) 和 Exit (即將退出Loop) 事件 。當(dāng)在操作 UI 時(shí),比如改變了 Frame、更新了 UIView/CALayer 的層次時(shí),或者手動(dòng)調(diào)用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,這個(gè) UIView/CALayer 就被標(biāo)記為待處理,并被提交到一個(gè)全局的容器去。當(dāng)Oberver監(jiān)聽的事件到來時(shí),回調(diào)執(zhí)行函數(shù)中會(huì)遍歷所有待處理的UIView/CAlayer 以執(zhí)行實(shí)際的繪制和調(diào)整,并更新 UI 界面。

如果此處有動(dòng)畫,通過 DisplayLink 穩(wěn)定的刷新機(jī)制會(huì)不斷的喚醒runloop,使得不斷的有機(jī)會(huì)觸發(fā)observer回調(diào),從而根據(jù)時(shí)間來不斷更新這個(gè)動(dòng)畫的屬性值并繪制出來。

函數(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];

繪圖和動(dòng)畫有兩種處理的方式:CPU(中央處理器)和GPU(圖形處理器)
CPU: CPU 中計(jì)算顯示內(nèi)容,比如視圖的創(chuàng)建、布局計(jì)算、圖片解碼、文本繪制等
GPU: GPU 進(jìn)行變換、合成、渲染.

關(guān)于CADisplayLink的描述有兩種

CADisplayLink 是一個(gè)和屏幕刷新率一致的定時(shí)器(但實(shí)際實(shí)現(xiàn)原理更復(fù)雜,和 NSTimer 并不一樣,其內(nèi)部實(shí)際是操作了一個(gè) Source)。如果在兩次屏幕刷新之間執(zhí)行了一個(gè)長(zhǎng)任務(wù),那其中就會(huì)有一幀被跳過去(和 NSTimer 相似),造成界面卡頓的感覺。在快速滑動(dòng)TableView時(shí),即使一幀的卡頓也會(huì)讓用戶有所察覺。

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

不管怎么樣CADisplayLink和NSTimer是有很大不同的,詳情可以參考這篇文章CADisplayLink

ibireme根據(jù)CADisplayLink的特性寫了個(gè)FPS指示器YYFPSLabel,代碼非常少
原理是這樣的:既然CADisplayLink可以以屏幕刷新的頻率調(diào)用指定selector,而且iOS系統(tǒng)中正常的屏幕刷新率為60Hz(60次每秒),所以使用 CADisplayLink 的 timestamp 屬性,配合 timer 的執(zhí)行次數(shù)計(jì)算得出FPS數(shù)

參考文章
深入理解RunLoop
iOS 事件處理機(jī)制與圖像渲染過程
RunLoop學(xué)習(xí)筆記(一) 基本原理介紹
iOS刨根問底-深入理解RunLoop
【iOS程序啟動(dòng)與運(yùn)轉(zhuǎn)】- RunLoop個(gè)人小結(jié)
RunLoop的前世今生
Runloop知識(shí)樹

最后編輯于
?著作權(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)容

  • 前言 RunLoop是iOS和OSX開發(fā)中非常基礎(chǔ)的一個(gè)概念,這篇文章將從CFRunLoop的源碼入手,介紹Run...
    暮年古稀ZC閱讀 2,404評(píng)論 1 19
  • 轉(zhuǎn)載:http://www.cocoachina.com/ios/20150601/11970.html RunL...
    Gatling閱讀 1,547評(píng)論 0 13
  • RunLoop 的概念 一般來講,一個(gè)線程一次只能執(zhí)行一個(gè)任務(wù),執(zhí)行完成后線程就會(huì)退出。如果我們需要一個(gè)機(jī)制,讓線...
    Mirsiter_魏閱讀 670評(píng)論 0 2
  • 原文地址:http://blog.ibireme.com/2015/05/18/runloop/ RunLoop ...
    大餅炒雞蛋閱讀 1,259評(píng)論 0 6
  • 生活陷入了新一輪的空虛,像從前,我怕是又陷入了想要睡覺的漩渦。毫無節(jié)制的吃,毫無節(jié)制的表達(dá)揮霍自己的健康和話語。 ...
    亞茹_我是阿茹閱讀 103評(píng)論 0 1

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