寫在前面的
為什么要了解 RunLoop?如果你想成為一個高級iOS開發工程師,那這是你必須了解的東西,他能幫助你更好的理解底層實現的原理,可以利用它的特性做出一些高效又神奇的功能。RunLoop這個東西已經是在各路大神的Blog里面描述和詳解過很多次的了,我把它翻出來再寫一遍,一來是為了讓自己溫故而知新,二來會重點詳細解讀一下當初我理解時候遇到的難點,為初、中級想要進階的iOS開發盆友排排坑。
本人寫的東西不是很好(從小語文沒學好),之前就懂的人看了肯定會覺得我很啰嗦(本人處女座,比較愛會啰嗦,不喜請跳過,我的寫博文的貫徹的理念是:寧肯讓大神們噴我啰嗦,也盡量讓不熟悉的人少點暈厥),我之前初次理解這塊的時候就想要別人越啰嗦越好,因為畢竟這塊東西對於剛開始了解底層的小伙伴來說看起來會比較暈厥(不管你暈沒暈,反正我當時是暈了)。如有大神路過,希望多多指點,共同學習。
總結:這是一篇可能會比較啰嗦的技術博文,我喜歡貼源代碼,這樣可以加深印象,鄙人難免有寫得不好或不對的地方,希望指出,樂於接受意見。
RunLoop的概念及作用
從字面意義上來看可以簡單的對它進行理解,Run就是跑,Loop就是圈,是的,這個就是對它最簡單的解釋——跑圈(這個是幾乎每個Blog都是這么寫的一個簡單概念)。
開始我先上段代碼:
int main() {
printf("hello world!\n");
return 1; }
這是大家在初學C語言編程的時候最常見的main函數運行的一段代碼,這里控制台會輸出相應的字符串,之后有一個return 1,return后程序就停止運行了。
那么問題就來了,當我打開一個APP后,我要的是它可以隨時響應我對他進行的各種操作,那前提肯定是整個APP會持續運行,只有持續運行我們才能監聽和處理各種事件,那么在iOS中什么東西能夠支撐APP持續運行呢,那就是我們的RunLoop了,就是這個東西讓我們的APP能持續跑圈,保證線程不被銷毀。
那怎么讓我們的APP跑圈呢,那我再上一段代碼:
int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } }
這段代碼相信作為iOS開發的大家也會非常的眼熟,他就是每個工程中main.m文件中的main函數。大家可以看到main函數return了一個UIApplicationMain函數的調用,這個函數在程序正常運行時不會有返回值,只有在程序退出時UIApplicationMain才會返回。而這個UIApplicationMain函數開啟了主線程的RunLoop(UIApplicationMain函數還做了其他很多事,關於其他詳細的東西我在這里就不多說了,推薦這篇文章,有興趣的可以看看),從而讓我們的主線程不會被銷毀,保證了程序的持續運行。
然而RunLoop除了讓線程持續運行不被銷毀以外,還會對線程性能做優化,這里涉及到一個模型叫做Event Loop,幾乎每個寫RunLoop的博文都會提到的這個東西,Event Loop在很多系統的框架里都有實現,在iOS中就是我們的RunLoop的實現,它的關鍵點在於:讓線程有任務的時候干活兒,沒任務的時候休眠,從而達到一個節省CPU資源,優化性能的目的。
總結:RunLoop的作用,這里先把其他大神總結出來的作用描述粘貼過來:
- 保持程序持續運行:例如程序一啟動就會開一個主線程,主線程一開起來就會跑一個主線程對應的 RunLoop , RunLoop 保證主線程不會被銷毀,也就保證了程序的持續運行;
- 處理 App 中的各種事件(比如:觸摸事件,定時器事件,Selector事件等 );
- 節省CPU資源,優化程序性能:程序運行起來時,當什么操作都沒有做的時候,RunLoop就通知系統,現在沒有事情做,然后進行休息待命狀態,這時系統就會將其資源釋放出來去做其他的事情。當有事情做,也就是一有響應的時候RunLoop就會立馬起來去做事情;
RunLoop與線程的關系
對於RunLoop和線程之間的關系,我先上一段蘋果Core Foundation中的開源代碼,源碼地址在這里,先看下面我摘出來的片段(有些東西看着可能會比較煩,可以直接看我的注釋):
//創建一個全局字典,用於保存線程對應的 RunLoop,key 是 pthread_t, value 是 CFRunLoopRef static CFMutableDictionaryRef __CFRunLoops = NULL; //訪問 loopsDic 時的鎖 static CFLock_t loopsLock = CFLockInit; // should only be called by Foundation // t==0 is a synonym for "main thread" that always works //獲取一個 pthread_t 對應的 CFRunLoopRef,此方法只能被 Foundation 框架調用 CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) { //如果傳入的 pthread_t 為 kNilPthreadT (kNilPthreadT 的定義 static pthread_t kNilPthreadT = { nil, nil };) ,則將傳入線程賦值為主線程 if (pthread_equal(t, kNilPthreadT)) { t = pthread_main_thread_np(); } __CFLock(&loopsLock); if (!__CFRunLoops) { // 第一次進入時,初始化全局字典,並先為主線程創建一個 RunLoop。 __CFUnlock(&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); __CFLock(&loopsLock); } //從全局字典中獲取 pthread_t 對應的 RunLoop CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); __CFUnlock(&loopsLock); if (!loop) { //如果字典里沒有就創建一個,並存入字典 CFRunLoopRef newLoop = __CFRunLoopCreate(t); __CFLock(&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 __CFUnlock(&loopsLock); CFRelease(newLoop); } if (pthread_equal(t, pthread_self())) { // 注冊一個回調,回調方法為 __CFFinalizeRunLoop,當線程銷毀時,順便也銷毀其對應的 RunLoop。 // 方法細節就不描述了,感興趣的小伙伴可以我上面貼出的蘋果開源代碼查看 _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL); if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) { _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop); } } return loop; } //在蘋果 Foundation 框架中,我們熟悉的 [NSRunLoop mainRunLoop] 方法,據我臆測應該就會調用 Core Foundation 框架中的此函數 CFRunLoopRef CFRunLoopGetMain(void) { CHECK_FOR_FORK(); static CFRunLoopRef __main = NULL; // no retain needed if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed return __main; } //在蘋果 Foundation 框架中,我們熟悉的 [NSRunLoop currentRunLoop] 方法,據我臆測應該就會調用 Core Foundation 框架中的此函數 CFRunLoopRef CFRunLoopGetCurrent(void) { CHECK_FOR_FORK(); CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop); if (rl) return rl; return _CFRunLoopGet0(pthread_self()); }
從上面的源碼可以得出以下幾點(主要看我的注釋),
1.線程和RunLoop之間他們是一對一的關系,其關系保存在一個全局的一個字典中。
2.線程不會自動創建RunLoop,必須主動獲取后才會被創建(啰嗦兩句,上面我貼出來的方法傳入的參數就是一個線程,只有把需要開啟RunLoop的線程傳入這個方法后,RunLoop才會被創建,如果需要加深印象理解,還是建議把上面的源碼多看幾遍)。
3.在對應線程結束時 RunLoop 會被銷毀。
總結:
- 每條線程都有且只有一個與之對應的 RunLoop 對象(RunLoop 跑圈的原理就是一個 do-while 的阻塞線程的循環,因此不可能在同一線程中同時有兩個RunLoop);
- RunLoop 在第一次獲取時創建,在線程結束時會被銷毀;只能在一個線程的內部獲取其 RunLoop(主線程除外)。
- 主線程的 RunLoop 系統默認啟動,子線程的 RunLoop 需要主動開啟;
RunLoop相關類及其詳解
在 CoreFoundation 里面關於RunLoop有五個類(其他先別管,多看幾遍,混個眼熟,反正這五個類是真的很重要):
-
CFRunLoopRef
-
CFRunLoopModeRef
-
CFRunLoopSourceRef
-
CFRunLoopTimerRef
-
CFRunLoopObserverRef
這五個類之間的關系,我就把網上都用的這張圖先貼過來:
簡單的描述一下這張圖(又開始啰嗦了),這張圖其實把RunLoop這重要的幾個類的結構表述的比較清楚了,藍色底的框框,就相當於是我們的RunLoop,五個類中的第一個,綠色底的框框就是Mode,五個類中的第二個。這里畫的在RunLoop中有兩個Mode,其實是可以有多個Mode,這個圖的后面可以加個省略號。在每個Mode的框里又有三個黃色的框,這三個框對應的就是這五個類中后面三個類了。
CFRunLoopRef 和 CFRunLoopModeRef
剛剛簡單的描述了一下他們的關系,但是第一次接觸他們的小伙伴可能還是會有一些懵逼。別着急,我先 簡單 的一一描述一下這五個類,先看前兩個類,我們慢慢用源代碼解釋這一切:
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; CFAbsoluteTime _runTime; CFAbsoluteTime _sleepTime; CFTypeRef _counterpart; }; struct __CFRunLoopMode { CFRuntimeBase _base; pthread_mutex_t _lock; /* must have the run loop locked before locking this */ CFStringRef _name; Boolean _stopped; char _padding[3]; CFMutableSetRef _sources0; CFMutableSetRef _sources1; CFMutableArrayRef _observers; CFMutableArrayRef _timers; CFMutableDictionaryRef _portToV1SourceMap; __CFPortSet _portSet; CFIndex _observerMask; #if USE_DISPATCH_SOURCE_FOR_TIMERS dispatch_source_t _timerSource; dispatch_queue_t _queue; Boolean _timerFired; // set to true by the source when a timer has fired Boolean _dispatchTimerArmed; #endif #if USE_MK_TIMER_TOO mach_port_t _timerPort; Boolean _mkTimerArmed; #endif #if DEPLOYMENT_TARGET_WINDOWS DWORD _msgQMask; void (*_msgPump)(void); #endif uint64_t _timerSoftDeadline; /* TSR */ uint64_t _timerHardDeadline; /* TSR */ };
以上是完整的 CFRunLoop 和 CFRunLoopMode 的結構體源碼(太長了我的媽,用不着看完),下面我精簡一下,把重要的留下,看如下代碼(可以仔細看一下,加深印象):
// RunLoopMode 數據結構 struct __CFRunLoopMode { CFStringRef _name; // Mode 名字, 唯一的標識,例如 kCFRunLoopDefaultMode CFMutableSetRef _sources0; // Set<CFRunLoopSourceRef> source0 集合 CFMutableSetRef _sources1; // Set<CFRunLoopSourceRef> source1 集合 CFMutableArrayRef _observers; // Array<CFRunLoopObserverRef> observer 數組 CFMutableArrayRef _timers; // Array<CFRunLoopTimerRef> timer 數組 ... }; // RunLoop 數據結構 struct __CFRunLoop { __CFPort _wakeUpPort; // 用來喚醒runLoop的端口,接收消息,執行CFRunLoopWakeUp方法 CFMutableSetRef _commonModes; // Set<CFStringRef> 標記為 Common 的 Mode 的集合 CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer> commonMode 的 items 集合 CFRunLoopModeRef _currentMode; // Current Runloop Mode. RunLoop 當前運行的 Mode CFMutableSetRef _modes; // Set<CFRunLoopModeRef> Mode 的集合 ... };
上面是精簡出來比較關鍵的 RunLoop 和 RunLoopMode 的結構體,從上面源碼可以看出:
一個 RunLoop 對象有一個用來被喚醒的端口 _wakeUpPort
,一個當前運行的 mode 叫 _currentMode
,以及若干個 _modes
、_commonModes
、_commonModeItems(commonModes這2個東西后面詳細講)
。runLoop 有很多 mode,即 _modes
,但是只有一個 _currentMode
,RunLoop 一次只能運行在一個 mode 下,如果需要切換 Mode,只能退出 Loop,不可能在多個 Mode 下同時運行(這是iOS運行流暢的原因之一)。
從 runLoopMode 的組成可以看出來:mode管理了所有的事件(Source/Timer/Observer 被稱為 Mode Item),而 RunLoop 管理着若干個 mode。
這兩個結構體中,已經涉及到了我們的所有五個類了,關於他們的關系我后面會詳細說,這里簡單的看看,對他們有個印象,混個臉熟,先來看 CFRunLoopSourceRef。
CFRunLoopSourceRef
在我 RunLoopMode 數據結構代碼中可以看到這兩個東西 CFMutableSetRef _source0 和 CFMutableSetRef _source1,首先這兩個東西是 Set(集合),集合中存放的是一堆數據結構(這里就可以對應上面藍色底那張圖來看,這是那種圖圖里面的Source集合的部分),那這個 source 到底是個什么東西呢,在 RunLoopMode 結構體的注釋中我也寫了,他們其實也是一個數據結構 CFRunLoopSourceRef。那 CFRunLoopSourceRef 結構又是怎樣的呢,我們再來看下面它的結構代碼:
struct __CFRunLoopSource { CFRuntimeBase _base; uint32_t _bits; //用於標記Signaled狀態,source0只有在被標記為Signaled狀態,才會被處理 pthread_mutex_t _lock; CFIndex _order; /* immutable */ CFMutableBagRef _runLoops; union { CFRunLoopSourceContext version0; /* source0的數據結構 */ CFRunLoopSourceContext1 version1; /* source1的數據結構 */ } _context; }; //source0 typedef struct { CFIndex version; // 版本號,用來區分是source1還是source0 void * info; const void *(*retain)(const void *info); void (*release)(const void *info); CFStringRef (*copyDescription)(const void *info); Boolean (*equal)(const void *info1, const void *info2); CFHashCode (*hash)(const void *info); void (*schedule)(void *info, CFRunLoopRef rl, CFRunLoopMode mode); void (*cancel)(void *info, CFRunLoopRef rl, CFRunLoopMode mode); void (*perform)(void *info); } CFRunLoopSourceContext; //source1 typedef struct { CFIndex version; // 版本號,用來區分是source1還是source0 void * info; const void *(*retain)(const void *info); void (*release)(const void *info); CFStringRef (*copyDescription)(const void *info); Boolean (*equal)(const void *info1, const void *info2); CFHashCode (*hash)(const void *info); #if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) || (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE) mach_port_t (*getPort)(void *info); // 端口 void * (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info); #else void * (*getPort)(void *info); void (*perform)(void *info); #endif } CFRunLoopSourceContext1;
上面代碼貼出來了三個數據結構,其他多余的別看,光看我注釋的部分就行,其中第一個數據結構 __CFRunLoopSource 包含一個 _context 成員,他的類型是 CFRunLoopSourceContext 或者是 CFRunLoopSourceContext1,也就是后面兩個數據結構。
大家可以從我重點看我注釋的行 CFRunLoopSourceContext(其實就是source0的數據結構)和 CFRunLoopSourceContext1(source1) 的區別就在於 CFRunLoopSourceContext1(source1) 多了一個 mach_port_t 接收消息的端口。mach_port_t 這又是個什么玩意兒,這里暫時不用管,可以簡單的啰嗦兩句,mach是iOS系統內核的心臟,他管理着處理器的資源,關於它的一些結構和原理,我以后會寫一篇文章來描述它的結構和工作原理,現在我還是把話收回來說主題,不走遠了。
這里簡單總結一下:
- CFRunLoopSourceRef 是事件產生的地方;
- 這個 CFRunLoopSourceRef 有兩個版本就是 source0 和 source1;
- source0只包含一個回調(函數指針),不能主動出發事件,需要 CFRunLoopSourceSignal(source) 將 Source 標記為待處理,CFRunLoopWakeUp(runloop) 喚醒 RunLoop,讓其處理事件
- source1包含 mach_port 和一個回調(函數指針),用於通過內核和其它線程相互發送消息,能主動喚醒 RunLoop。
CFRunLoopObserver
這個東西從名字上來看是觀察者,我們先來看看他的結構體:
struct __CFRunLoopObserver { CFRuntimeBase _base; pthread_mutex_t _lock; CFRunLoopRef _runLoop; //observer對應的runLoop CFIndex _rlCount; //observer當前監測的runLoop數量 CFOptionFlags _activities; //observer觀測runLoop的狀態,枚舉類型 CFIndex _order; //CFRunLoopMode中是數組形式存儲的observer,_order就是他在數組中的位置 CFRunLoopObserverCallBack _callout; //observer觀察者的函數回調 CFRunLoopObserverContext _context; /* immutable, except invalidation */ };
CFRunLoopObserver 是觀察者,每個 Observer 都包含了一個回調(函數指針),當 RunLoop 的狀態(CFOptionFlags _activities)發生變化時,觀察者就能通過回調函數接收到狀態的變化。那么 RunLoop 的狀態(CFOptionFlags)又有哪些呢:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = (1UL << 0), // 即將進入Loop kCFRunLoopBeforeTimers = (1UL << 1), // runLoop即將處理 Timers kCFRunLoopBeforeSources = (1UL << 2), // runLoop即將處理 Sources kCFRunLoopBeforeWaiting = (1UL << 5), // runLoop即將進入休眠 kCFRunLoopAfterWaiting = (1UL << 6), // runLoop剛從休眠中喚醒 kCFRunLoopExit = (1UL << 7), // 即將退出RunLoop kCFRunLoopAllActivities = 0x0FFFFFFFU };
這些就是 RunLoop 的狀態們, 我們通過添加 CFRunLoopObserver 就能對其狀態進行監聽。在實際應用中,通過監聽的結果我們就能在更新某個狀態的時候做一些事情。
CFRunLoopTimer
還是一樣,先來看結構:
struct __CFRunLoopTimer { CFRuntimeBase _base; uint16_t _bits; pthread_mutex_t _lock; CFRunLoopRef _runLoop; //所在的RunLoop CFMutableSetRef _rlModes; CFAbsoluteTime _nextFireDate; CFTimeInterval _interval; /* immutable */ CFTimeInterval _tolerance; /* mutable */ uint64_t _fireTSR; /* TSR units */ CFIndex _order; /* immutable */ CFRunLoopTimerCallBack _callout; //函數回調 CFRunLoopTimerContext _context; /* immutable, except invalidation */ };
看中文注釋就行了,這個 CFRunLoopTimer 就跟我們平時用的 NSTimer 是一個東西,它是一個基於時間的觸發器,包含了時間長度和回調函數,當 timer 加入 RunLoop 后,RunLoop 會注冊對應的時間點,在時間點上,RunLoop 會被喚醒執行 timer 中的函數回調。
到這里關於 RunLoop 的五個類已經簡單的描述完了(我又要開始啰嗦了),在講五個類之間的關系前,我還要先說另一個概念 RunLoopMode 的 Mode(這里我避免大家把 RunLoopMode 的 Mode 和后面要講的 commonMode 搞混了,我在這里把他看做 RunLoopMode 的“名字”,下面再詳細說明一下這個東西)
CFStringRef _name
(這一部分特別容易攪暈,我盡量多啰嗦一點,有不清楚的地方可以多看幾遍)回過頭來看 __CFRunLoopMode 結構體,這里面的第一個成員是 CFStringRef _name(這是 RunLoopMode 的唯一標識(“名字”),他的類型是 Core Foundation 框架中的 String),RunLoopMode 都是通過“名字”來創建的,你傳入 Mode 的“名字” RunLoop 會給你創建對應的 RunLoopMode,那 RunLoopMode 的“名字”又有哪些,這個“名字”主要有以下五種(為什么說主要有這五種,因為在APP的主線啟動時對應的 RunLoop 會默認創建這五種,后面我會進行驗證,當然肯定不止五種,這里可以看到更多的蘋果內部的 RunLoopMode 的“名字”):
kCFRunLoopDefaultMode //默認模式,通常主線程在這個模式下運行 UITrackingRunLoopMode //界面跟蹤Mode,用於追蹤Scrollview觸摸滑動時的狀態。 kCFRunLoopCommonModes //占位符,帶有Common標記的字符串,比較特殊的一個mode; UIInitializationRunLoopMode //剛啟動App時進入的第一個Mode,啟動后不在使用。 GSEventReceiveRunLoop //內部Mode,接收系事件。
在這里我建了個工程(我本來是想找蘋果 Darwin 源碼來看 RunLoop 在啟動時候的情況的,但是可能因為太笨了,死活沒找到,那就笨人用笨辦法來驗證吧),用來驗證 RunLoop 創建后它的 RunLoopMode 的情況。首先我再工程的 ViewController 的 ViewDidLoad 中寫了如下代碼:
這段代碼就是獲取了當前主線程的 RunLoop ,並且輸出他的詳情,在運行后,我在控制台得到了一個特別長(特別特別長)的結果(感興趣的朋友可以操作一下,再用json格式化一下),我這里就放幾個關鍵的截圖,用來驗證APP啟動后,主線程 RunLoop 中的 RunLoopMode 的初始化情況:
這個是 CFRunLoopRef 結構體中 modes 成員的 description 的開頭部分,我們可以看到他的數量是5,我們再分別看他里面的成員:
至此我們可以驗證出在主線程中(注意子線程並不會被默認創建這5個)這5個 RunLoopMode 被蘋果默認創建。在這5個 RunLoopMode 中有一個 Mode 比較特殊,那就是 kCFRunLoopCommonModes。如果操作打印了 RunLoop 詳情的朋友可能會發現他與其他四個 Mode 都不同,我先把它的形態貼出來:
先補充一個概念,Source/Timer/Observer(就是我們介紹的五個類中的后三個) 被統為 Mode Item,如果一個 Mode 中沒有一個 Item,而 RunLoop 又在這個 Mode 下運行,那么這個 RunLoop 將會退出,不會進入循環。
從上面的結構可以看到它的 Source/Timer/Observer 全部為空,顯然他不是一個正常 Mode,所以在上面對他的注釋中寫到的,他是一個占位符,沒有實際作用(具體作用我后面會詳細說到,先混個臉熟)。
Common Modes
蘋果官方文檔里找到的一個概念名詞,這個東西容易被攪暈,很多Blog里都沒有說得很清楚,更有的說的直接就是錯的。Common Modes 先看名字,前面是一個 “Common” 后面是一個 “Modes”,簡單的理解為被貼了“Common”標簽的 Mode 們的集合(因為他是 Modes,一個復數),而蘋果官方跟這個集合起了個名字就叫 “Common Modes”。但是實際上 Mode 的結構體里面並沒有地方讓你貼這個“Common”標簽,那我怎么知道 Mode 是其中一個 Common Modes,我們倒回去看結構體 CFRunLoopRef (可以翻到上面再看看他的結構)中的 _commonModes 成員,它是一個 CFMutableSetRef 類型的,這個集合中存的是 Mode 的“名字”,而此集合就可以理解為是 Common Modes 的名單。而值得注意的是 Common Modes 是保存在 RunLoop 中的,而不是在 Mode 中。
在看 CFRunLoopRef 結構體的時候肯定還會看到一個叫 _commonModeItems 的成員,它也是一個集合,它里面存放的是 Mode Item(Source/Timer/Observer)。_commonModes 和 _commonModeItems 兩個集合是相互有關聯的,他們有這樣的關系:
- 當 _commonModes 集合中添加了一個 Mode,那么這個新添加的 Mode 會把 _commonModeItems 中所有的 Mode Item(Source/Timer/Observer)添加到自己當中。
- 當 _commonModeItems 集合中添加了一個 Mode Item(Source/Timer/Observer),那么這個 Item 將會被添加到 _commonModes 集合中所有的 Mode 中。
這個東西說起來比較抽象,還是把源碼貼上來,給大家加深印象,先看第一個關系:
//向 runLoop 的 commonModes 添加一個 mode void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName) { CHECK_FOR_FORK(); if (__CFRunLoopIsDeallocating(rl)) return; __CFRunLoopLock(rl); // 判斷 modeName 是否在_commonModes 中,如果已經存在,else中不做任何處理 if (!CFSetContainsValue(rl->_commonModes, modeName)) { // 拷貝 runloop 的 _commonModeItems 集合 CFSetRef set = rl->_commonModeItems ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModeItems) : NULL; //往 _commonModes 中添加 mode 的 名字 CFSetAddValue(rl->_commonModes, modeName); // 如果items 存在 if (NULL != set) { CFTypeRef context[2] = {rl, modeName}; /* add all common-modes items to new mode */ //把從 _commonModeItems 拷貝的集合中的 mode item 們添加到這個 mode 中 CFSetApplyFunction(set, (__CFRunLoopAddItemsToCommonMode), (void *)context); CFRelease(set); } } else { } __CFRunLoopUnlock(rl); } // 把一個 mode item 添加到指定的 mode 中 static void __CFRunLoopAddItemsToCommonMode(const void *value, void *ctx) { CFTypeRef item = (CFTypeRef)value; CFRunLoopRef rl = (CFRunLoopRef)(((CFTypeRef *)ctx)[0]); CFStringRef modeName = (CFStringRef)(((CFTypeRef *)ctx)[1]); // 判斷 item 具體是哪種類型,然后進行添加 if (CFGetTypeID(item) == CFRunLoopSourceGetTypeID()) { CFRunLoopAddSource(rl, (CFRunLoopSourceRef)item, modeName); } else if (CFGetTypeID(item) == CFRunLoopObserverGetTypeID()) { CFRunLoopAddObserver(rl, (CFRunLoopObserverRef)item, modeName); } else if (CFGetTypeID(item) == CFRunLoopTimerGetTypeID()) { CFRunLoopAddTimer(rl, (CFRunLoopTimerRef)item, modeName); } }
代碼略長,看我注釋就夠了,再把上面的總結粘貼到這里:當 _commonModes 集合中添加了一個 Mode,那么這個新添加的 Mode 會把 _commonModeItems 中所有的 Mode Item(Source/Timer/Observer)添加到自己當中。
下面再來看第二個關系的源碼:
//往指定的 RunLoop 中的 Mode 中添加 source void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) { /* DOES CALLOUT */ CHECK_FOR_FORK(); if (__CFRunLoopIsDeallocating(rl)) return; if (!__CFIsValid(rls)) return; Boolean doVer0Callout = false; __CFRunLoopLock(rl); //如果傳入需要添加的 mode 名字為 kCFRunLoopCommonModes if (modeName == kCFRunLoopCommonModes) { //拷貝 _commonModes 拿到 Common Modes 的名單 CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL; //如果 _commonModeItems 為空就創建 if (NULL == rl->_commonModeItems) { rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks); } //先把 source 添加到 _commonModeItems 中 CFSetAddValue(rl->_commonModeItems, rls); if (NULL != set) { CFTypeRef context[2] = {rl, rls}; /* add new item to all common-modes */ //再按 Common Modes 的名單挨個添加該 source CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context); CFRelease(set); } } else { //不是往 _commonModeItems 添加 item 這里就省略了 ... } }
只看注釋就好,代碼混個臉熟,這樣印象深刻,代碼的總結我再粘貼一遍:當 _commonModeItems 集合中添加了一個 Mode Item(Source/Timer/Observer),那么這個 Item 將會被添加到 _commonModes 集合中所有的 Mode 中。
在APP主線程中對應的 RunLoop 中,有兩個 Mode 被放到了 _commonModes 中,它們是 kCFRunLoopDefaultMode 和 UITrackingRunLoopMode(僅限主線程,我們自己創建的子線程只有 kCFRunLoopDefaultMode)。放下面一張圖來點直觀的印象(看最右邊一列,看哪些是Part of common modes):
kCFRunLoopDefaultMode 是 App 平時所處的狀態,UITrackingRunLoopMode 是追蹤 ScrollView 滑動時的狀態。
說了這么多,那這個 Common Modes 到底是干嘛的,哪些場景會用到。我這里還是把那個經典例子拿出來說一下,當你創建一個 Timer 並加到 kCFRunLoopDefaultMode 時(Timer必須要加入 RunLoop 中才會跑起來),Timer 會得到重復回調,但此時滑動一個TableView時,RunLoop 的 mode 切換為 TrackingRunLoopMode,這時 Timer 就不會被回調,並且也不會影響到滑動操作。
但是往往你會想要 Timer 不管TableView是否在滾動都能回調方法,一種辦法就是將這個 Timer 分別加入這兩個 Mode。還有一種方式,就是將 Timer 加入到的 RunLoop 的 _commonModeItems 中,(再次貼出剛才總結)當 _commonModeItems 集合中添加了一個 Mode Item(Source/Timer/Observer),那么這個 Item 將會被添加到 _commonModes 集合中所有的 Mode 中(主線程 RunLoop 的 Common Modes 中有 kCFRunLoopDefaultMode 和 UITrackingRunLoopMode ),那么這個經典問題就被愉快的解決了。
關於 Common Modes 最后再啰嗦補充一下,要想將某個 Mode 變成 Common Modes ,CoreFoundation 框架提供的方法是:
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
而如果要將某個 Mode Item(Source/Timer/Observer)加入 RunLoop 的 _commonModeItems 中,可以調用 框架提供的方法:
CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef modeName);
其中 modeName 參數傳 kCFRunLoopCommonModes 即可,或者是調用 Foundation 框架中的方法:
- (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode; - (void)addPort:(NSPort *)aPort forMode:(NSRunLoopMode)mode;
其中 mode 參數傳 NSRunLoopCommonModes 即可。
RunLoop的內部邏輯
他的內部邏輯各種大牛也詳細的闡述過,因為源代碼太長了,有幾百行,我這里就不一比一的貼出來了,只貼出大致的重點邏輯代碼,這里涉及到 RunLoop 內部邏輯的函數主要有三個:
第一個函數,我們指定一個 Mode 來運行 RunLoop 會調用以下方法:
/** * 根據指定運行模式 mode 運行 RunLoop */ SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { CHECK_FOR_FORK(); return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled); }
第二個函數,會調用上面代碼中的 CFRunLoopRunSpecific 方法,它的返回值和上面的函數是一樣的,是一個 SInt32
類型的值,根據返回值,來決定 RunLoop 的運行狀況:
//在指定的 mode 下,運行指定的 runLoop SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { // 根據rl,modeName 獲取指定的 currentMode CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false); // 1. 如果當前 mode 不存在,或者當前 Mode 中沒有 Mode Item(source/timer/observer),即返回 kCFRunLoopRunFinished if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) { // 聲明一個標識did,默認false Boolean did = false; // did 為 false,返回 kCFRunLoopRunFinished return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished; } // 初始化一個返回結果,值為kCFRunLoopRunFinished int32_t result = kCFRunLoopRunFinished; // 2.通知 observers 即將開始循環 if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry); // 運行 RunLoop 主體 result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode); // 3.通知 observers 即將退出循環runLoop if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
return result; }
此段代碼省去了這個方法的細節部分,提煉出來了重點的三個部分即注釋中的1,2,3:
- 如果當前 mode 不存在,或者當前 Mode 中沒有 Mode Item(source/timer/observer),即函數返回 kCFRunLoopRunFinished
- 通知 observers 即將開始循環
- 通知 observers 即將退出循環 RunLoop
第三個函數,這個函數是 RunLoop 的主體函數,處理了 RunLoop 整個生命周期的所有邏輯,代碼原版有幾百行,這里省去了大部分,從它的核心 do-while 循環開始的,可以主要看注釋(如果覺得太長的可以跳過看后面的“大眾”總結圖):
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
...
// 聲明一個標識,默認true,用於執行消息處理 Boolean didDispatchPortLastTime = true; // 聲明一個返回值,用於最后的結果返回 int32_t retVal = 0; // do..while循環主體,處理runLoop的邏輯 do { // 1. RunLoop 即將處理 Timers, 通知 observers if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers); // 2. RunLoop 即將處理 Sources,通知 observers if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources); // 3. RunLoop 開始處理 Source0事件 // sourceHandledThisLoop 是否處理完Source0事件 Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle); if (sourceHandledThisLoop) { // 處理完Source0之后的回調 __CFRunLoopDoBlocks(rl, rlm); } // 處理完source0事件,且沒有超時 poll 為false, // 沒有處理完source0 事件,或者超時,為true Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR); // didDispatchPortLastTime 初始化為true,即第一次循環的時候不會走if方法, // 4. 消息處理,source1 事件,此處跳過休眠,直接到下面代碼的第8步 if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) { // 從消息緩沖區獲取消息 msg = (mach_msg_header_t *)msg_buffer; // dispatchPort收到消息,立刻去處理 // dispatchPort 主線程接收消息的端口 if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) { // 收到消息,跳過下面休眠,goto第8步處理消息 goto handle_msg; } } // didDispatchPortLastTime 設置為false,以便進行消息處理 didDispatchPortLastTime = false; // 5. 通知 observers runLoop即將休眠 if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting); // runLoop 休眠 __CFRunLoopSetSleeping(rl); // 6.線程進入休眠, 直到被下面某一個事件喚醒: // a. 基於 port 的 Source1 的事件 // b. Timer 到時間了 // c. RunLoop 啟動時設置的最大超時時間到了 // d. 被手動喚醒 do { // 從消息緩沖區獲取消息 msg = (mach_msg_header_t *)msg_buffer; // 內部調用 mach_msg() 等待接受 waitSet 的消息 __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy); } while (1); // 設置rl不再等待喚醒 __CFRunLoopSetIgnoreWakeUps(rl); // runloop 醒來 __CFRunLoopUnsetSleeping(rl); // 7. 已被喚醒,通知observers if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting); // 8. 處理消息 handle_msg:; // 設置rl不再等待喚醒 __CFRunLoopSetIgnoreWakeUps(rl); // 判斷 livePort // 8.1 如果不存在 if (MACH_PORT_NULL == livePort) { CFRUNLOOP_WAKEUP_FOR_NOTHING(); // 8.2 如果是喚醒rl的端口,回到第1步 } else if (livePort == rl->_wakeUpPort) { CFRUNLOOP_WAKEUP_FOR_WAKEUP(); ResetEvent(rl->_wakeUpPort); } // 定時器事件__CFRunLoopDoTimers // 8.3 如果是定時器的端口 else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) { // 處理定時器事件 CFRUNLOOP_WAKEUP_FOR_TIMER(); if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) { __CFArmNextTimerInMode(rlm, rl); } } // 8.4. 如果端口是主線程的端口,直接處理 else if (livePort == dispatchPort) { CFRUNLOOP_WAKEUP_FOR_DISPATCH(); __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg); } else { // 8.5. 除上述4點之外的端口 CFRUNLOOP_WAKEUP_FOR_SOURCE(); // 從端口收到的消息事件,為source1事件 CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort); if (rls) { mach_msg_header_t *reply = NULL; // 處理source1 事件 sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop; if (NULL != reply) { // 消息處理, // message.h中,以后有時間會再研究一下 (void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL); } } } // 9. 判斷是否要退出 RunLoop if (sourceHandledThisLoop && stopAfterHandle) { // 9.1 如果事件處理完就退出,並且source處理完成 retVal = kCFRunLoopRunHandledSource; } else if (timeout_context->termTSR < mach_absolute_time()) { // 9.2 超時退出 retVal = kCFRunLoopRunTimedOut; } else if (__CFRunLoopIsStopped(rl)) { // 9.3 調用 CFRunLoopStop() 退出 __CFRunLoopUnsetStopped(rl); retVal = kCFRunLoopRunStopped; } else if (rlm->_stopped) { // 9.4 runLoopMode 狀態停止 rlm->_stopped = false; retVal = kCFRunLoopRunStopped; } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) { // 9.5 Mode Item(source/timer/observer) 一個都沒有了 retVal = kCFRunLoopRunFinished; } // 上述幾種情況,會跳出do..while循環, // 除此之外,繼續循環 } while (0 == retVal); return retVal; }
從上面代碼總結來看,我們的 RunLoop 其實就是一個 do-while 循環,在這個循環的過程中不斷的處理各種消息,直到超時或者被手動停止,該函數才會返回。下面這張在網上隨處可見的總結圖其實對 RunLoop 的整個過程描述的非常清楚了,下面上圖先:
(備注:左邊黃色的地方,“source0 (port) ”改為”source1 (port)”)
看圖說話,總結而論,在__CFRunLoopRun()
中,先初始化RunLoop超時機制,然后進入do-while循環,循環內,處理source0,看情況決定是否真正進入休眠,喚醒后(或者根本沒休眠),查詢是否有 Timer 需要執行、是否有主線程 Port 消息需要轉發、是否有 source1 需要執行,在關鍵的步驟上都會通知 observer 有狀態更新,而后根據參數和執行情況更新返回值,最后根據返回值確定是否繼續循環。
什么時候使用RunLoop
說了那么多關於 RunLoop 的東西, 那么平時什么時候會用到它呢?用它又可以干些什么神奇的事兒呢?先來看看我們平時最常用到的:
NSTimer
經常用到的定時器,這個跟上面說到的 CFRunLoopTimerRef 他們是 Toll-Free Bridge (免費橋,指 Core Foundation 對象與 Objective-C 對象之間的一種轉換,這種轉換不需要使用額外的 CPU 資源,可以簡單的理解為他們骨子里就是同一種東西),timer 必須要注冊到一個活的 RunLoop 中才能有效的回調。
PerformSelecter
這個方法平時我們也經常用到,然而這個方法有帶不同參數的寫法,這里就舉個例子,比如我們調用 NSObject 的 performSelecter:afterDelay: 方法,它的原理是在內部創建了一個 Timer 並添加到當前線程的 RunLoop 中的 kCFRunLoopDefaultMode 里(performSelecter 的方法不都是為 RunLoop 創建 Timer,不要誤解了),既然是 Timer ,那么這里就有兩個問題,第一個如果這個 performSelecter 運行在一個沒有開啟 RunLoop 的子線程中,那么這個方法就會失效;第二個問題,如果 performSelecter 運行在主線程中,而 afterDelay 設置了一個時間,而時間點到時,如果用戶正在滑動 TableView ,那么它並不會准時執行方法。
GCD相關
平時寫多線程最常用到的就是GCD了,上面說到的 Timer 其實他的內部也是通過 dispatch_source_t 來實現的,而調用 dispatch_async(dispatch_get_main_queue(), block) 方法時,其內部實現是會喚醒 RunLoop ,從消息中得到 block,並回調(僅限於主線程,子線程依然會用 GCD 自己的 libDispatch 來處理)。
界面更新
所有其他大牛的博客里都說到了這個東西,我也簡單說一下,蘋果更新UI的邏輯用到了 RunLoop 中的 Observer,CA注冊了對 RunLoop 的監聽,Observer 監聽了 BeforeWaiting(即將進入休眠) 事件,並在 BeforeWaiting 的時候進行了 UI 的更新。
上面說到了平時開發中 RunLoop 的實際體現,不過上面涉及到的很多時候我們都只是用到,而並可能沒有感受到它真正的存在,也沒有說到真正要手動開啟一個 RunLoop 的實例,那么哪些時候需要我們手動開啟一個 RunLoop 呢?
- 使用端口或自定義輸入源來和其他線程通信
- 在子線程中使用定時器
- 使線程周期性工作
上面這些應用,這里暫時不講了,也寫了這么長了 ,此篇文章主要講一下 RunLoop 的原理和概念,后面我會專門再寫一篇文章來講 RunLoop 的應用。本人第一篇博文,肯定有寫得不好的地方,也有說得不清楚的地方,后面會盡量完善...
————————————————
本文參考:深入理解RunLoop,RunLoop個人小結