RunLoop看這篇就夠了

RunLoop 的概念

通常來說,一個線程一次只能執行一個任務,執行完成後線程就會退出。若是咱們須要一個機制,讓線程能隨時處理事件但並不退出, 一般的代碼邏輯是這樣的:html

function loop() {
    initialize();
    do {
        var message = get_next_message();
        process_message(message);
    } while (message != quit);
}
複製代碼

這種模型一般被稱做 Event Loop。 Event Loop 在不少系統和框架裏都有實現,好比 Node.js 的事件處理,好比 Windows 程序的消息循環,再好比 OSX/iOS 裏的 RunLoop。實現這種模型的關鍵點在於:如何管理事件/消息,如何讓線程在沒有處理消息時休眠以免資源佔用、在有消息到來時馬上被喚醒。 因此,RunLoop 實際上就是一個對象,這個對象管理了其須要處理的事件和消息,並提供了一個入口函數來執行上面 Event Loop 的邏輯。線程執行了這個函數後,就會一直處於這個函數內部 「接受消息->等待->處理」 的循環中,直到這個循環結束(好比傳入 quit 的消息),函數返回。 OSX/iOS 系統中,提供了兩個這樣的對象:NSRunLoop 和 CFRunLoopRef。 CFRunLoopRef 是在 CoreFoundation 框架內的,它提供了純 C 函數的 API,全部這些 API 都是線程安全的。 NSRunLoop 是基於 CFRunLoopRef 的封裝,提供了面向對象的 API,可是這些 API 不是線程安全的。 CFRunLoopRef 的代碼是開源的,你能夠在這裏 opensource.apple.com/tarballs/CF… 下載到整個 CoreFoundation 的源碼來查看。 (Update: Swift 開源後,蘋果又維護了一個跨平臺的 CoreFoundation 版本:github.com/apple/swift…,這個版本的源碼可能和現有 iOS 系統中的實現略不同,但更容易編譯,並且已經適配了 Linux/Windows。)前端

一、RunLoop的做用?RunLoop和線程的關係?

1.1 RunLoop的做用

RunLoop官方文檔.png

  • 保持程序的持續運行ios

  • 處理APP中的各類事件(觸摸、定時器、performSelector)git

  • 節省cpu資源、提供程序的性能:該作事就作事,該休息就休息。github

1.2 RunLoop和線程的關係

首先,iOS 開發中能遇到兩個線程對象: pthread_tNSThread。過去蘋果有份文檔標明瞭 NSThread 只是 pthread_t 的封裝,但那份文檔已經失效了,如今它們也有可能都是直接包裝自最底層的 mach thread。蘋果並無提供這兩個對象相互轉換的接口,但無論怎麼樣,能夠確定的是 pthread_tNSThread 是一一對應的。好比,你能夠經過 pthread_main_thread_np()[NSThread mainThread] 來獲取主線程;也能夠經過 pthread_self()[NSThread currentThread] 來獲取當前線程。CFRunLoop 是基於pthread 來管理的。蘋果不容許直接建立 RunLoop,它只提供了兩個自動獲取的函數:CFRunLoopGetMain()CFRunLoopGetCurrent()。 這兩個函數內部的邏輯大概是下面這樣:swift

/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 訪問 loopsDic 時的鎖
static CFSpinLock_t loopsLock;
 
/// 獲取一個 pthread 對應的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
    OSSpinLockLock(&loopsLock);
    
    if (!loopsDic) {
        // 第一次進入時,初始化全局Dic,並先爲主線程建立一個 RunLoop。
        loopsDic = CFDictionaryCreateMutable();
        CFRunLoopRef mainLoop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
    }
    
    /// 直接從 Dictionary 裏獲取。
    CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
    
    if (!loop) {
        /// 取不到時,建立一個
        loop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, thread, loop);
        /// 註冊一個回調,當線程銷燬時,順便也銷燬其對應的 RunLoop。
        _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
    }
    
    OSSpinLockUnLock(&loopsLock);
    return loop;
}
 
CFRunLoopRef CFRunLoopGetMain() {
    return _CFRunLoopGet(pthread_main_thread_np());
}
 
CFRunLoopRef CFRunLoopGetCurrent() {
    return _CFRunLoopGet(pthread_self());
}
複製代碼

從上面的代碼能夠看出,線程和RunLoop之間是一一對應的,以key-value的形式保存在一個全局的 Dictionary 裏。線程剛建立時並無RunLoop,若是你不主動獲取,那它一直都不會有。RunLoop 的建立是發生在第一次獲取時,RunLoop 的銷燬是發生在線程結束時。你只能在一個線程的內部獲取其 RunLoop(主線程除外)。segmentfault

子線程不建立RunLoop默認不開啓。安全

經過 [[NSRunLoop currentRunLoop] run];開啓子線程的RunLoopbash

timer依賴於RunLoop,能夠經過結束線程來銷燬定時器。網絡

static CFMutableDictionaryRef __CFRunLoops = NULL;
static CFSpinLock_t loopsLock = CFSpinLockInit;

// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
   if (pthread_equal(t, kNilPthreadT)) {
   t = pthread_main_thread_np();
   }
   __CFSpinLock(&loopsLock);
   if (!__CFRunLoops) {
       __CFSpinUnlock(&loopsLock);
   CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
   CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
   CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
   if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
       CFRelease(dict);
   }
   CFRelease(mainLoop);
       __CFSpinLock(&loopsLock);
   }
   CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
   __CFSpinUnlock(&loopsLock);
   if (!loop) {
   CFRunLoopRef newLoop = __CFRunLoopCreate(t);
       __CFSpinLock(&loopsLock);
   loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
   if (!loop) {
       CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
       loop = newLoop;
   }
       // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it __CFSpinUnlock(&loopsLock); CFRelease(newLoop); } if (pthread_equal(t, pthread_self())) { _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL); if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) { _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop); } } return loop; } 複製代碼

二、RunLoop對外的接口

在 CoreFoundation 裏面關於 RunLoop 有5個類:

CFRunLoopRef

CFRunLoopModeRef

CFRunLoopSourceRef

CFRunLoopTimerRef

CFRunLoopObserverRef

其中 CFRunLoopModeRef 類並無對外暴露,只是經過 CFRunLoopRef 的接口進行了封裝。他們的關係以下:

經過源碼分析能夠看到__CFRunLoop是個結構體對象

struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;			/* locked for accessing mode list */
    __CFPort _wakeUpPort;			// used for CFRunLoopWakeUp 
    Boolean _unused;
    volatile _per_run_data *_perRunData;              // reset for runs of the run loop
    pthread_t _pthread;
    uint32_t _winthread;
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFTypeRef _counterpart;
};

複製代碼

一個 RunLoop 包含若干個 Mode ,每一個Mode 又包含若干個 Source/Timer/Observer 。每次調用 RunLoop 的主函數時,只能指定其中一個 Mode ,這個Mode 被稱做 CurrentMode 。若是須要切換 Mode ,只能退出 Loop ,再從新指定一個 Mode 進入。這樣作主要是爲了分隔開不一樣組的 Source/Timer/Observer ,讓其互不影響。

2.1 CFRunLoopSourceRef

  • CFRunLoopSourceRef 是事件產生的地方。Source 有兩個版本:Source0Source1

  • Source0 只包含了一個回調(函數指針),它並不能主動觸發事件。使用時,你須要先調用 CFRunLoopSourceSignal(source) ,將這個 Source 標記爲待處理,而後手動調用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop ,讓其處理這個事件。

  • Source1 包含了一個 mach_port 和一個回調(函數指針),被用於經過內核和其餘線程相互發送消息。這種 Source 能主動喚醒 RunLoop 的線程,其原理在下面會講到。

2.2 CFRunLoopTimerRef

CFRunLoopTimerRef 是基於時間的觸發器,它和 NSTimertoll-free bridged 的,能夠混用。其包含一個時間長度和一個回調(函數指針)。當其加入到 RunLoop 時,RunLoop 會註冊對應的時間點,當時間點到時,RunLoop 會被喚醒以執行那個回調。

2.3 CFRunLoopObserverRef

CFRunLoopObserverRef 是觀察者,每一個 Observer 都包含了一個回調(函數指針),當 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
};
複製代碼

上面的 Source/Timer/Observer 被統稱爲 mode item,一個 item 能夠被同時加入多個 mode。但一個 item 被重複加入同一個 mode 時是不會有效果的。若是一個 mode 中一個 item 都沒有,則 RunLoop 會直接退出,不進入循環

三、RunLoop 的 Mode

CFRunLoopMode 和 CFRunLoop 的結構大體以下:

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

這裏有個概念叫 「CommonModes」:一個 Mode 能夠將本身標記爲」Common」屬性(經過將其 ModeName 添加到 RunLoop「commonModes」 中)。每當 RunLoop 的內容發生變化時,RunLoop 都會自動將 _commonModeItems 裏的 Source/Observer/Timer 同步到具備 「Common」 標記的全部Mode裏。

應用場景舉例:主線程的 RunLoop 裏有兩個預置的 Mode:kCFRunLoopDefaultModeUITrackingRunLoopMode。這兩個 Mode 都已經被標記爲」Common」屬性。DefaultMode 是 App 平時所處的狀態,TrackingRunLoopMode 是追蹤 ScrollView 滑動時的狀態。當你建立一個 Timer 並加到 DefaultMode 時,Timer 會獲得重複回調,但此時滑動一個TableView時,RunLoop 會將 mode 切換爲 TrackingRunLoopMode,這時 Timer 就不會被回調,而且也不會影響到滑動操做。

有時你須要一個 Timer,在兩個 Mode 中都能獲得回調,一種辦法就是將這個 Timer 分別加入這兩個 Mode。還有一種方式,就是將 Timer 加入到頂層的 RunLoop「commonModeItems」 中。」commonModeItems」RunLoop 自動更新到全部具備」Common」屬性的 Mode 裏去。

CFRunLoop對外暴露的管理 Mode 接口只有下面2個:

CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);
複製代碼

Mode 暴露的管理 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);
複製代碼

你只能經過 mode name 來操做內部的 mode,當你傳入一個新的 mode nameRunLoop 內部沒有對應 mode 時,RunLoop會自動幫你建立對應的 CFRunLoopModeRef。對於一個 RunLoop 來講,其內部的mode` 只能增長不能刪除。

蘋果公開提供的 Mode 有兩個:kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和 UITrackingRunLoopMode,你能夠用這兩個 Mode Name 來操做其對應的Mode

同時蘋果還提供了一個操做Common標記的字符串:kCFRunLoopCommonModes (NSRunLoopCommonModes),你能夠用這個字符串來操做Common Items,或標記一個Mode「Common」。使用時注意區分這個字符串和其餘mode name

RunLoop 的內部邏輯 根據蘋果在文檔裏的說明,RunLoop 內部的邏輯大體以下:

其內部代碼整理以下 (太長了不想看能夠直接跳過去,後面會有說明):

/// 用DefaultMode啓動
void CFRunLoopRun(void) {
    CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}
 
/// 用指定的Mode啓動,容許設置RunLoop超時時間
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
 
/// RunLoop的實現
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
    
    /// 首先根據modeName找到對應mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
    /// 若是mode裏沒有source/timer/observer, 直接返回。
    if (__CFRunLoopModeIsEmpty(currentMode)) return;
    
    /// 1. 通知 Observers: RunLoop 即將進入 loop。
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
    
    /// 內部函數,進入loop
    __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
        
        Boolean sourceHandledThisLoop = NO;
        int retVal = 0;
        do {
 
            /// 2. 通知 Observers: RunLoop 即將觸發 Timer 回調。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
            /// 3. 通知 Observers: RunLoop 即將觸發 Source0 (非port) 回調。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
            /// 執行被加入的block
            __CFRunLoopDoBlocks(runloop, currentMode);
            
            /// 4. RunLoop 觸發 Source0 (非port) 回調。
            sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
            /// 執行被加入的block
            __CFRunLoopDoBlocks(runloop, currentMode);
 
            /// 5. 若是有 Source1 (基於port) 處於 ready 狀態,直接處理這個 Source1 而後跳轉去處理消息。
            if (__Source0DidDispatchPortLastTime) {
                Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
                if (hasMsg) goto handle_msg;
            }
            
            /// 通知 Observers: RunLoop 的線程即將進入休眠(sleep)。
            if (!sourceHandledThisLoop) {
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
            }
            
            /// 7. 調用 mach_msg 等待接受 mach_port 的消息。線程將進入休眠, 直到被下面某一個事件喚醒。
            /// • 一個基於 port 的Source 的事件。
            /// • 一個 Timer 到時間了
            /// • RunLoop 自身的超時時間到了
            /// • 被其餘什麼調用者手動喚醒
            __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 若是一個 Timer 到時間了,觸發這個Timer的回調。
            if (msg_is_timer) {
                __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
            } 
 
            /// 9.2 若是有dispatch到main_queue的block,執行block。
            else if (msg_is_dispatch) {
                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            } 
 
            /// 9.3 若是一個 Source1 (基於port) 發出事件了,處理這個事件
            else {
                CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
                sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
                if (sourceHandledThisLoop) {
                    mach_msg(reply, MACH_SEND_MSG, reply);
                }
            }
            
            /// 執行加入到Loop的block
            __CFRunLoopDoBlocks(runloop, currentMode);
            
 
            if (sourceHandledThisLoop && stopAfterHandle) {
                /// 進入loop時參數說處理完事件就返回。
                retVal = kCFRunLoopRunHandledSource;
            } else if (timeout) {
                /// 超出傳入參數標記的超時時間了
                retVal = kCFRunLoopRunTimedOut;
            } else if (__CFRunLoopIsStopped(runloop)) {
                /// 被外部調用者強制中止了
                retVal = kCFRunLoopRunStopped;
            } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
                /// source/timer/observer一個都沒有了
                retVal = kCFRunLoopRunFinished;
            }
            
            /// 若是沒超時,mode裏沒空,loop也沒被中止,那繼續loop。
        } while (retVal == 0);
    }
    
    /// 10. 通知 Observers: RunLoop 即將退出。
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}
複製代碼

能夠看到,實際上 RunLoop 就是這樣一個函數,其內部是一個 do-while 循環。當你調用 CFRunLoopRun() 時,線程就會一直停留在這個循環裏;直到超時或被手動中止,該函數纔會返回。

四、RunLoop 的底層實現

從上面代碼能夠看到,RunLoop 的核心是基於 mach port 的,其進入休眠時調用的函數是 mach_msg()。爲了解釋這個邏輯,下面稍微介紹一下 OSX/iOS 的系統架構。

蘋果官方將整個系統大體劃分爲上述4個層次:

  • 應用層包括用戶能接觸到的圖形應用,例如 SpotlightAquaSpringBoard 等。

  • 應用框架層:即開發人員接觸到的 Cocoa 等框架。

  • 核心框架層:包括各類核心框架OpenGL 等內容。

Darwin 即操做系統的核心,包括系統內核、驅動、Shell 等內容,這一層是開源的,其全部源碼均可以在 opensource.apple.com 裏找到。

咱們在深刻看一下 Darwin 這個核心的架構:

其中,在硬件層上面的三個組成部分:MachBSDIOKit (還包括一些上面沒標註的內容),共同組成了 XNU 內核。 XNU內核的內環被稱做 Mach,其做爲一個微內核,僅提供了諸如處理器調度、IPC (進程間通訊)等很是少許的基礎服務。 BSD 層能夠看做圍繞Mach 層的一個外環,其提供了諸如進程管理、文件系統和網絡等功能。 IOKit 層是爲設備驅動提供了一個面向對象(C++)的一個框架。

Mach 自己提供的 API 很是有限,並且蘋果也不鼓勵使用 MachAPI,可是這些API很是基礎,若是沒有這些API的話,其餘任何工做都沒法實施。在 Mach 中,全部的東西都是經過本身的對象實現的,進程、線程和虛擬內存都被稱爲」對象」。和其餘架構不一樣, Mach 的對象間不能直接調用,只能經過消息傳遞的方式實現對象間的通訊。」消息」是 Mach 中最基礎的概念,消息在兩個端口 (port) 之間傳遞,這就是 MachIPC (進程間通訊) 的核心。

Mach 的消息定義是在 <mach/message.h> 頭文件的,很簡單:

typedef struct {
  mach_msg_header_t header;
  mach_msg_body_t body;
} mach_msg_base_t;
 
typedef struct {
  mach_msg_bits_t msgh_bits;
  mach_msg_size_t msgh_size;
  mach_port_t msgh_remote_port;
  mach_port_t msgh_local_port;
  mach_port_name_t msgh_voucher_port;
  mach_msg_id_t msgh_id;
} mach_msg_header_t;
複製代碼

一條 Mach 消息實際上就是一個二進制數據包 (BLOB),其頭部定義了當前端口 local_port 和目標端口 remote_port, 發送和接受消息是經過同一個 API 進行的,其 option 標記了消息傳遞的方向:

mach_msg_return_t mach_msg(
			mach_msg_header_t *msg,
			mach_msg_option_t option,
			mach_msg_size_t send_size,
			mach_msg_size_t rcv_size,
			mach_port_name_t rcv_name,
			mach_msg_timeout_t timeout,
			mach_port_name_t notify);
複製代碼

爲了實現消息的發送和接收,mach_msg() 函數其實是調用了一個 Mach 陷阱 (trap),即函數mach_msg_trap(),陷阱這個概念在 Mach 中等同於系統調用。當你在用戶態調用 mach_msg_trap() 時會觸發陷阱機制,切換到內核態;內核態中內核實現的 mach_msg() 函數會完成實際的工做,以下圖:

這些概念能夠參考維基百科: System_callTrap_(computing)

RunLoop 的核心就是一個 mach_msg() (見上面代碼的第7步),RunLoop 調用這個函數去接收消息,若是沒有別人發送 port 消息過來,內核會將線程置於等待狀態。例如你在模擬器裏跑起一個 iOS 的 App,而後在 App 靜止時點擊暫停,你會看到主線程調用棧是停留在 mach_msg_trap() 這個地方。 關於具體的如何利用 mach port 發送信息,能夠看看 NSHipster 這一篇文章,或者這裏的中文翻譯 。 關於Mach的歷史能夠看看這篇頗有趣的文章:Mac OS X 背後的故事(三)Mach 之父 Avie Tevanian

四、蘋果用 RunLoop 實現的功能

首先咱們能夠看一下 App 啓動後 RunLoop 的狀態:

CFRunLoop {
    current mode = kCFRunLoopDefaultMode
    common modes = {
        UITrackingRunLoopMode
        kCFRunLoopDefaultMode
    }
 
    common mode items = {
 
        // source0 (manual)
        CFRunLoopSource {order =-1, {
            callout = _UIApplicationHandleEventQueue}}
        CFRunLoopSource {order =-1, {
            callout = PurpleEventSignalCallback }}
        CFRunLoopSource {order = 0, {
            callout = FBSSerialQueueRunLoopSourceHandler}}
 
        // source1 (mach port)
        CFRunLoopSource {order = 0,  {port = 17923}}
        CFRunLoopSource {order = 0,  {port = 12039}}
        CFRunLoopSource {order = 0,  {port = 16647}}
        CFRunLoopSource {order =-1, {
            callout = PurpleEventCallback}}
        CFRunLoopSource {order = 0, {port = 2407,
            callout = _ZL20notify_port_callbackP12__CFMachPortPvlS1_}}
        CFRunLoopSource {order = 0, {port = 1c03,
            callout = __IOHIDEventSystemClientAvailabilityCallback}}
        CFRunLoopSource {order = 0, {port = 1b03,
            callout = __IOHIDEventSystemClientQueueCallback}}
        CFRunLoopSource {order = 1, {port = 1903,
            callout = __IOMIGMachPortPortCallback}}
 
        // Ovserver
        CFRunLoopObserver {order = -2147483647, activities = 0x1, // Entry
            callout = _wrapRunLoopWithAutoreleasePoolHandler}
        CFRunLoopObserver {order = 0, activities = 0x20,          // BeforeWaiting
            callout = _UIGestureRecognizerUpdateObserver}
        CFRunLoopObserver {order = 1999000, activities = 0xa0,    // BeforeWaiting | Exit
            callout = _afterCACommitHandler}
        CFRunLoopObserver {order = 2000000, activities = 0xa0,    // BeforeWaiting | Exit
            callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv}
        CFRunLoopObserver {order = 2147483647, activities = 0xa0, // BeforeWaiting | Exit
            callout = _wrapRunLoopWithAutoreleasePoolHandler}
 
        // Timer
        CFRunLoopTimer {firing = No, interval = 3.1536e+09, tolerance = 0,
            next fire date = 453098071 (-4421.76019 @ 96223387169499),
            callout = _ZN2CAL14timer_callbackEP16__CFRunLoopTimerPv (QuartzCore.framework)}
    },
 
    modes = {
        CFRunLoopMode  {
            sources0 =  { /* same as 'common mode items' */ },
            sources1 =  { /* same as 'common mode items' */ },
            observers = { /* same as 'common mode items' */ },
            timers =    { /* same as 'common mode items' */ },
        },
 
        CFRunLoopMode  {
            sources0 =  { /* same as 'common mode items' */ },
            sources1 =  { /* same as 'common mode items' */ },
            observers = { /* same as 'common mode items' */ },
            timers =    { /* same as 'common mode items' */ },
        },
 
        CFRunLoopMode  {
            sources0 = {
                CFRunLoopSource {order = 0, {
                    callout = FBSSerialQueueRunLoopSourceHandler}}
            },
            sources1 = (null),
            observers = {
                CFRunLoopObserver >{activities = 0xa0, order = 2000000,
                    callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv}
            )},
            timers = (null),
        },
 
        CFRunLoopMode  {
            sources0 = {
                CFRunLoopSource {order = -1, {
                    callout = PurpleEventSignalCallback}}
            },
            sources1 = {
                CFRunLoopSource {order = -1, {
                    callout = PurpleEventCallback}}
            },
            observers = (null),
            timers = (null),
        },
        
        CFRunLoopMode  {
            sources0 = (null),
            sources1 = (null),
            observers = (null),
            timers = (null),
        }
    }
}
複製代碼

能夠看到,系統默認註冊了5個Mode:

  • kCFRunLoopDefaultMode: App的默認 Mode,一般主線程是在這個 Mode 下運行的。

  • UITrackingRunLoopMode: 界面跟蹤 Mode,用於 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其餘 Mode 影響。

  • UIInitializationRunLoopMode: 在剛啓動 App 時第進入的第一個 Mode,啓動完成後就再也不使用。

  • GSEventReceiveRunLoopMode: 接受系統事件的內部 Mode,一般用不到。

  • kCFRunLoopCommonModes: 這是一個佔位的 Mode,沒有實際做用。

你能夠在這裏看到更多的蘋果內部的 Mode,但那些 Mode 在開發中就很難遇到了。

當 RunLoop 進行回調時,通常都是經過一個很長的函數調用出去 (call out), 當你在你的代碼中下斷點調試時,一般能在調用棧上看到這些函數。下面是這幾個函數的整理版本,若是你在調用棧中看到這些長函數名,在這裏查找一下就能定位到具體的調用地點了:

{
    /// 1. 通知Observers,即將進入RunLoop
    /// 此處有Observer會建立AutoreleasePool: _objc_autoreleasePoolPush();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
    do {
 
        /// 2. 通知 Observers: 即將觸發 Timer 回調。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
        /// 3. 通知 Observers: 即將觸發 Source (非基於port的,Source0) 回調。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
 
        /// 4. 觸發 Source0 (非基於port的) 回調。
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
 
        /// 6. 通知Observers,即將進入休眠
        /// 此處有Observer釋放並新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
 
        /// 7. sleep to wait msg.
        mach_msg() -> mach_msg_trap();
        
 
        /// 8. 通知Observers,線程被喚醒
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
 
        /// 9. 若是是被Timer喚醒的,回調Timer
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
 
        /// 9. 若是是被dispatch喚醒的,執行全部調用 dispatch_async 等方法放入main queue 的 block
        __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
 
        /// 9. 若是若是Runloop是被 Source1 (基於port的) 的事件喚醒了,處理這個事件
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
 
 
    } while (...);
 
    /// 10. 通知Observers,即將退出RunLoop
    /// 此處有Observer釋放AutoreleasePool: _objc_autoreleasePoolPop();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}
複製代碼

五、AutoreleasePool

App啓動後,蘋果在主線程 RunLoop 裏註冊了兩個 Observer,其回調都是 _wrapRunLoopWithAutoreleasePoolHandler()。

第一個 Observer 監視的事件是 Entry(即將進入Loop),其回調內會調用 _objc_autoreleasePoolPush() 建立自動釋放池。其 order 是-2147483647,優先級最高,保證建立釋放池發生在其餘全部回調以前。

第二個 Observer 監視了兩個事件: BeforeWaiting(準備進入休眠) 時調用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池並建立新池;Exit(即將退出Loop) 時調用 _objc_autoreleasePoolPop() 來釋放自動釋放池。這個 Observer 的 order 是 2147483647,優先級最低,保證其釋放池子發生在其餘全部回調以後。

在主線程執行的代碼,一般是寫在諸如事件回調、Timer回調內的。這些回調會被 RunLoop 建立好的 AutoreleasePool 環繞着,因此不會出現內存泄漏,開發者也沒必要顯示建立 Pool 了。

六、RunLoop主要處理如下6類事件:

block應用:CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK

調用timer:CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION

響應source0:CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION

響應source1: CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION

GCD主隊列:CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE

observer源:CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION

6.1 事件響應

蘋果註冊了一個 Source1 (基於 mach port 的) 用來接收系統事件,其回調函數爲 __IOHIDEventSystemClientQueueCallback()。

當一個硬件事件(觸摸/鎖屏/搖晃等)發生後,首先由 IOKit.framework 生成一個 IOHIDEvent 事件並由 SpringBoard 接收。這個過程的詳細狀況能夠參考這裏。SpringBoard 只接收按鍵(鎖屏/靜音等),觸摸,加速,接近傳感器等幾種 Event,隨後用 mach port 轉發給須要的App進程。隨後蘋果註冊的那個 Source1 就會觸發回調,並調用 _UIApplicationHandleEventQueue() 進行應用內部的分發。

_UIApplicationHandleEventQueue() 會把 IOHIDEvent 處理幷包裝成 UIEvent 進行處理或分發,其中包括識別 UIGesture/處理屏幕旋轉/發送給 UIWindow 等。一般事件好比 UIButton 點擊、touchesBegin/Move/End/Cancel 事件都是在這個回調中完成的。

6.2 手勢識別

當上面的 _UIApplicationHandleEventQueue() 識別了一個手勢時,其首先會調用 Cancel 將當前的 touchesBegin/Move/End 系列回調打斷。隨後系統將對應的 UIGestureRecognizer 標記爲待處理。

蘋果註冊了一個 Observer 監測 BeforeWaiting (Loop即將進入休眠) 事件,這個Observer的回調函數是 _UIGestureRecognizerUpdateObserver(),其內部會獲取全部剛被標記爲待處理的 GestureRecognizer,並執行GestureRecognizer的回調。

當有 UIGestureRecognizer 的變化(建立/銷燬/狀態改變)時,這個回調都會進行相應處理。

6.3 界面更新

當在操做 UI 時,好比改變了 Frame、更新了 UIView/CALayer 的層次時,或者手動調用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法後,這個 UIView/CALayer 就被標記爲待處理,並被提交到一個全局的容器去。

蘋果註冊了一個 Observer 監聽 BeforeWaiting(即將進入休眠) 和 Exit (即將退出Loop) 事件,回調去執行一個很長的函數: _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。這個函數裏會遍歷全部待處理的 UIView/CAlayer 以執行實際的繪製和調整,並更新 UI 界面。

這個函數內部的調用棧大概是這樣的:

_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];
複製代碼

6.4 定時器

NSTimer 其實就是 CFRunLoopTimerRef,他們之間是 toll-free bridged 的。一個 NSTimer 註冊到 RunLoop 後,RunLoop 會爲其重複的時間點註冊好事件。例如 10:00, 10:10, 10:20 這幾個時間點。RunLoop爲了節省資源,並不會在很是準確的時間點回調這個Timer。Timer 有個屬性叫作 Tolerance (寬容度),標示了當時間點到後,允許有多少最大偏差。

若是某個時間點被錯過了,例如執行了一個很長的任務,則那個時間點的回調也會跳過去,不會延後執行。就好比等公交,若是 10:10 時我忙着玩手機錯過了那個點的公交,那我只能等 10:20 這一趟了。

CADisplayLink 是一個和屏幕刷新率一致的定時器(但實際實現原理更復雜,和 NSTimer 並不同,其內部實際是操做了一個 Source)。若是在兩次屏幕刷新之間執行了一個長任務,那其中就會有一幀被跳過去(和 NSTimer 類似),形成界面卡頓的感受。在快速滑動TableView時,即便一幀的卡頓也會讓用戶有所察覺。Facebook 開源的 AsyncDisplayLink 就是爲了解決界面卡頓的問題,其內部也用到了 RunLoop。

6.5 PerformSelecter

當調用 NSObject 的 performSelecter:afterDelay: 後,實際上其內部會建立一個 Timer 並添加到當前線程的 RunLoop 中。因此若是當前線程沒有 RunLoop,則這個方法會失效。

當調用 performSelector:onThread: 時,實際上其會建立一個 Timer 加到對應的線程去,一樣的,若是對應線程沒有 RunLoop 該方法也會失效。

6.6 關於GCD

實際上 RunLoop 底層也會用到 GCD 的東西,好比 RunLoop 是用 dispatch_source_t 實現的 Timer(評論中有人提醒,NSTimer 是用了 XNU 內核的 mk_timer,我也仔細調試了一下,發現 NSTimer 確實是由 mk_timer 驅動,而非 GCD 驅動的)。但同時 GCD 提供的某些接口也用到了 RunLoop, 例如 dispatch_async()。

當調用 dispatch_async(dispatch_get_main_queue(), block) 時,libDispatch 會向主線程的 RunLoop 發送消息,RunLoop會被喚醒,並從消息中取得這個 block,並在回調 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 裏執行這個 block。但這個邏輯僅限於 dispatch 到主線程,dispatch 到其餘線程仍然是由 libDispatch 處理的。

6.6 關於網絡請求

iOS 中,關於網絡請求的接口自下至上有以下幾層:

CFSocket
CFNetwork       ->ASIHttpRequest
NSURLConnection ->AFNetworking
NSURLSession    ->AFNetworking2, Alamofire
複製代碼
  • CFSocket 是最底層的接口,只負責 socket 通訊。
  • CFNetwork 是基於 CFSocket 等接口的上層封裝,ASIHttpRequest 工做於這一層。
  • NSURLConnection 是基於 CFNetwork的更高層的封裝,提供面向對象的接口,AFNetworking 工做於這一層。
  • NSURLSession 是 iOS7 中新增的接口,表面上是和 NSURLConnection 並列的,但底層仍然用到了 NSURLConnection 的部分功能 (好比 com.apple.NSURLConnectionLoader 線程),AFNetworking2.0Alamofire 工做於這一層。

下面主要介紹下 NSURLConnection 的工做過程。

一般使用 NSURLConnection 時,你會傳入一個 Delegate,當調用了[connection start] 後,這個 Delegate 就會不停收到事件回調。實際上,start 這個函數的內部會會獲取 CurrentRunLoop,而後在其中的 DefaultMode 添加了4個 Source0 (即須要手動觸發的Source)。CFMultiplexerSource 是負責各類 Delegate 回調的,CFHTTPCookieStorage 是處理各類 Cookie 的。

當開始網絡傳輸時,咱們能夠看到 NSURLConnection 建立了兩個新線程:com.apple.NSURLConnectionLoadercom.apple.CFSocket.private。其中 CFSocket 線程是處理底層socket 鏈接的。NSURLConnectionLoader 這個線程內部會使用 RunLoop 來接收底層 socket 的事件,並經過以前添加的 Source0通知到上層的 Delegate

NSURLConnectionLoader 中的 RunLoop 經過一些基於 mach portSource 接收來自底層 CFSocket 的通知。當收到通知後,其會在合適的時機向 CFMultiplexerSourceSource0 發送通知,同時喚醒 Delegate 線程的 RunLoop 來讓其處理這些通知。CFMultiplexerSource 會在 Delegate 線程的 RunLoopDelegate執行實際的回調。

七、RunLoop 的實際應用舉例

AFNetworking2.0中的應用

AFURLConnectionOperation 這個類是基於 NSURLConnection 構建的,其但願能在後臺線程接收 Delegate 回調。爲此 AFNetworking 單首創建了一個線程,並在這個線程中啓動了一個 RunLoop

+ (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 啓動前內部必需要有至少一個 Timer/Observer/Source,因此AFNetworking[runLoop run] 以前先建立了一個新的 NSMachPort 添加進去了。一般狀況下,調用者須要持有這個 NSMachPort (mach_port) 並在外部線程經過這個port 發送消息到loop 內;但此處添加 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];
}
複製代碼

當須要這個後臺線程執行任務時,AFNetworking2.0 經過調用 [NSObject performSelector:onThread:..] 將這個任務扔到了後臺線程的 RunLoop 中。

參考

iOS 事件處理機制與圖像渲染過程

RunLoop學習筆記(一) 基本原理介紹

iOS刨根問底-深刻理解RunLoop

深刻理解RunLoop

【iOS程序啓動與運轉】- RunLoop我的小結

RunLoop的前世此生

Runloop知識樹

runloop原理

深刻理解runloop

線程安全類的設計

iOS保持界面流暢的技巧和AsyncDisplay介紹

離屏渲染

ios核心動畫高級技巧

相關文章
相關標籤/搜索