RunLoop深度探究(一)

原文鏈接:http://yangchao0033.github.io/blog/2016/01/06/runloopshen-du-tan-jiu/

RunLoop的概念

一般來講,一個線程一次只能執行一個任務,執行完成后線程就會退出。如果我們需要一個機制,讓線程能隨時處理事件但并不退出,通常的代碼邏輯是這樣的:

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 的代碼是開源的,你可以在這里 http://opensource.apple.com/tarballs/CF/CF-855.17.tar.gz 下載到整個 CoreFoundation 的源碼。為了方便跟蹤和查看,你可以新建一個 Xcode 工程,把這堆源碼拖進去看。

RunLoop與線程的關系

首先,iOS 開發中能遇到兩個線程對象: pthread_t 和 NSThread。過去蘋果有份文檔標明了 NSThread 只是 pthread_t 的封裝,但那份文檔已經失效了,現在它們也有可能都是直接包裝自最底層的 mach thread。蘋果并沒有提供這兩個對象相互轉換的接口,但不管怎么樣,可以肯定的是 pthread_t 和 NSThread 是一一對應的。比如,你可以通過 pthread_main_thread_np() 或 [NSThread mainThread] 來獲取主線程;也可以通過 pthread_self() 或 [NSThread currentThread] 來獲取當前線程。CFRunLoop 是基于 pthread 來管理的。

蘋果不允許直接創建 RunLoop,它只提供了兩個自動獲取的函數:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。 這兩個函數內部的邏輯大概是下面這樣:

/// 全局的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之間是一一對應的,并且關系是保存在一個全局的字典中的,并且線程剛創建時是沒有RunLoop的,如果你不獲取它,他一直都不會有,RunLoop的創建發生在第一次獲取的時候,RunLoop的銷毀發生在線程結束時。并且只能在線程內部獲取runloop(主線程除外)。

RunLoop 對外的接口

在 CoreFoundation 里面關于 RunLoop 有5個類:

CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef

其中 CFRunLoopModeRef 類并沒有對外暴露,只是通過 CFRunLoopRef 的接口進行了封裝。他們的關系如下:


image
image

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

CFRunLoopSourceRef是事件產生的地方。Source有兩個版本:Source0 和 Source1。
? Source0 只包含了一個回調(函數指針),它并不能主動觸發事件。使用時,你需要先調用 CFRunLoopSourceSignal(source),將這個 Source 標記為待處理,然后手動調用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件。
? Source1 包含了一個 mach_port 和一個回調(函數指針),被用于通過內核和其他線程相互發送消息。這種 Source 能主動喚醒 RunLoop 的線程,其原理在下面會講到。

image
image

CFRunLoopTimerRef是基于時間的觸發器,他和NSTimer是toll-free bridged的,可以混用。其包含一個時間長度和一個回調(函數指針)。

image
image

CFRunLoopObserverRef是觀察者,每個Observer都包含了一個回調(函數指針),當RunLoop的狀態發生變法時,觀察者就能通過回調接受這個變化。可以觀測的時間點有以下幾個:

image
image

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的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”中)。每當RnuLoop的內容發生變化時,RunLoop的都會自動將_commonModeItems里的Source/Timer/Observer同步到具有“Common”標記的所有的Mode。
應用舉例:主線程 RunLoop 默認會預制兩個 Mode :kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。這兩個Mode都已經被標記為“Common”屬性。DefaultMode是App平時所處的狀態,TrackingRunLoop是為了追蹤ScrollView滑動時的狀態。當你創建一個Timer并加到DefaultMode時,Timer會得到重復回調。此時滑動TableView時,RunLoop會將mode切換為TrackingRunLoopMode,這時Timer就不會被回調,也不會影響滑動的操作了。具體用例類似于在tableView中加入滾動廣告欄,當你在操作tableView時會回調自動滾動欄的Timer,造成滾動欄的滑動出現卡頓。

有時你需要一個Timer,在兩個Mode中都能得到回調,一種辦法就是講這個Timer加入兩個Mode。還有一種就是講Timer加入到頂層的RunLoop的“commonModeItems”中。“commonModeItems”被RunLoop自動更新到所有具有“Common”屬性的Mode里去。

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

CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName); // 給RunLoop添加到CommonMode中
CFRunLoopRunInMode(CFStringRef modeName, ...); // 返回當前線程中指定mode的CFRunLoop對象

Mode暴露的管理mode item的接口有下面幾個

CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);// 添加一個CFRunLoopSource對象到一個run loop mode中(如果添加的Source是source0的話,這個方法將會調用 schedule 回調在source的上下文結構(context structure)的指定方法)。一個runloop source 可以同時被注冊到多個 runloop 和 runloop modes 中。當source被發出信號,無論哪一個被注冊的 runloop 都會開始檢測第一個發出信號的 source 。 如過rl的mode中已經包含source時,這個方法將不會做任何事。
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName); // 添加CFRunLoopObserver對象到一個run loop mode中去。 討論:一個 runloop 觀察者只能被同時注冊在一個 runloop 中,盡管它可以被通過他的tunloop添加到多個runloop modes中。 如果rl已經在 mode中 包含 obsever 中,這個方法將不會做任何事。
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName); // 添加CFRunLoopTimer 對象到一個runloop mode中 討論:一個runloop timer 在同一時刻只能注冊在一個run loop,盡管它可以被通過他的tunloop添加到多個runloop modes中。 如果rl已經在 mode中 包含 obsever 中,這個方法將不會做任何事
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName); // 從run loop mode 移除 Observer 對象,如果 rl 沒有包含參數中的Observer,則該函數不做任何處理
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode); // 從run loop mode 移除 timer 對象,如果 rl 沒有包含參數中的timer,則該函數不做任何處理

以上接口可以看出,只能通過mode name操作內部mode,當你傳入一個新的mode name但runloop內部沒有對應的mode時,runloop會自動幫你創建對應的CFRunloopModeRef。并且官方文檔明確指出,對于runloop來說,其內部的mode只能增加不能刪除。

蘋果官方公開的內部mode有兩個:CFRunLoopDefaultMode(NSDefaultRunLoopMode)和UITrackingRunLoopMode,你可以用這兩個 Mode Name來操作對應的 Mode。
同時蘋果還提出了一個操作Common標記的字符串:kCFRunLoopCommonModes(NSRunLoopCommonModes),你可以用這個字符串來操作Common Items,或標記一個Mode為“Common”。使用時注意區分該字符串與其他mode name。

特別致謝:

http://blog.ibireme.com/
2015/05/18/runloop/#more-41710

參考文章:

深入理解RunLoop:

http://blog.ibireme.com/2015/05/18/runloop/#more-41710

Apple Document:

https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容