原文鏈接:http://yangchao0033.github.io/blog/2016/01/06/runloopshen-du-tan-jiu/
RunLoop的概念
一般來講,一個線程一次只能執(zhí)行一個任務,執(zhí)行完成后線程就會退出林说。如果我們需要一個機制,讓線程能隨時處理事件但并不退出询枚,通常的代碼邏輯是這樣的:
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
這種模型通常被稱作 Event Loop。 Event Loop 在很多系統(tǒng)和框架里都有實現浙巫,比如 Node.js 的事件處理金蜀,比如 Windows 程序的消息循環(huán)刷后,再比如 OSX/iOS 里的 RunLoop。實現這種模型的關鍵點在于:如何管理事件/消息渊抄,如何讓線程在沒有處理消息時休眠以避免資源占用尝胆、在有消息到來時立刻被喚醒。
所以护桦,RunLoop 實際上就是一個對象含衔,這個對象管理了其需要處理的事件和消息,并提供了一個入口函數來執(zhí)行上面 Event Loop 的邏輯二庵。線程執(zhí)行了這個函數后贪染,就會一直處于這個函數內部 "接受消息->等待->處理" 的循環(huán)中,直到這個循環(huán)結束(比如傳入 quit 的消息)催享,函數返回杭隙。
OSX/iOS 系統(tǒng)中,提供了兩個這樣的對象: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 開發(fā)中能遇到兩個線程對象: pthread_t 和 NSThread。過去蘋果有份文檔標明了 NSThread 只是 pthread_t 的封裝朽缴,但那份文檔已經失效了善玫,現在它們也有可能都是直接包裝自最底層的 mach thread。蘋果并沒有提供這兩個對象相互轉換的接口密强,但不管怎么樣茅郎,可以肯定的是 pthread_t 和 NSThread 是一一對應的。比如或渤,你可以通過 pthread_main_thread_np() 或 [NSThread mainThread] 來獲取主線程系冗;也可以通過 pthread_self() 或 [NSThread currentThread] 來獲取當前線程。CFRunLoop 是基于 pthread 來管理的薪鹦。
蘋果不允許直接創(chuàng)建 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华临,并先為主線程創(chuàng)建一個 RunLoop芯杀。
loopsDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
}
/// 直接從 Dictionary 里獲取。
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
if (!loop) {
/// 取不到時银舱,創(chuàng)建一個
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之間是一一對應的棋弥,并且關系是保存在一個全局的字典中的,并且線程剛創(chuàng)建時是沒有RunLoop的诚欠,如果你不獲取它顽染,他一直都不會有,RunLoop的創(chuàng)建發(fā)生在第一次獲取的時候轰绵,RunLoop的銷毀發(fā)生在線程結束時粉寞。并且只能在線程內部獲取runloop(主線程除外)。
RunLoop 對外的接口
在 CoreFoundation 里面關于 RunLoop 有5個類:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
其中 CFRunLoopModeRef 類并沒有對外暴露左腔,只是通過 CFRunLoopRef 的接口進行了封裝唧垦。他們的關系如下:
一個 RunLoop 包含若干個 Mode,每個 Mode 又包含若干個 Source/Timer/Observer液样。每次調用 RunLoop 的主函數時振亮,只能指定其中一個 Mode,這個Mode被稱作 CurrentMode鞭莽。如果需要切換 Mode坊秸,只能退出 Loop,再重新指定一個 Mode 進入澎怒。這樣做主要是為了分隔開不同組的 Source/Timer/Observer褒搔,讓其互不影響。
CFRunLoopSourceRef是事件產生的地方喷面。Source有兩個版本:Source0 和 Source1星瘾。
? Source0 只包含了一個回調(函數指針),它并不能主動觸發(fā)事件惧辈。使用時死相,你需要先調用 CFRunLoopSourceSignal(source),將這個 Source 標記為待處理咬像,然后手動調用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop算撮,讓其處理這個事件生宛。
? Source1 包含了一個 mach_port 和一個回調(函數指針),被用于通過內核和其他線程相互發(fā)送消息肮柜。這種 Source 能主動喚醒 RunLoop 的線程陷舅,其原理在下面會講到。
CFRunLoopTimerRef是基于時間的觸發(fā)器审洞,他和NSTimer是toll-free bridged的莱睁,可以混用。其包含一個時間長度和一個回調(函數指針)芒澜。
CFRunLoopObserverRef是觀察者仰剿,每個Observer都包含了一個回調(函數指針),當RunLoop的狀態(tài)發(fā)生變法時痴晦,觀察者就能通過回調接受這個變化南吮。可以觀測的時間點有以下幾個:
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的內容發(fā)生變化時部凑,RunLoop的都會自動將_commonModeItems里的Source/Timer/Observer同步到具有“Common”標記的所有的Mode。
應用舉例:主線程 RunLoop 默認會預制兩個 Mode :kCFRunLoopDefaultMode 和 UITrackingRunLoopMode碧浊。這兩個Mode都已經被標記為“Common”屬性涂邀。DefaultMode是App平時所處的狀態(tài),TrackingRunLoop是為了追蹤ScrollView滑動時的狀態(tài)箱锐。當你創(chuàng)建一個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被發(fā)出信號瞭稼,無論哪一個被注冊的 runloop 都會開始檢測第一個發(fā)出信號的 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會自動幫你創(chuàng)建對應的CFRunloopModeRef。并且官方文檔明確指出摔握,對于runloop來說寄狼,其內部的mode只能增加不能刪除。
蘋果官方公開的內部mode有兩個:CFRunLoopDefaultMode(NSDefaultRunLoopMode)和UITrackingRunLoopMode氨淌,你可以用這兩個 Mode Name來操作對應的 Mode泊愧。
同時蘋果還提出了一個操作Common標記的字符串:kCFRunLoopCommonModes(NSRunLoopCommonModes),你可以用這個字符串來操作Common Items盛正,或標記一個Mode為“Common”删咱。使用時注意區(qū)分該字符串與其他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: