這篇博客主要結(jié)合Apple開發(fā)者文檔和個(gè)人的理解,寫的一篇關(guān)于Cocoa RunLoop基本知識點(diǎn)的文章。在文檔的基礎(chǔ)上,概況和梳理了RunLoop相關(guān)的知識點(diǎn)。
一、Event Loop & Cocoa RunLoop
宏觀上:Event Loop
- RunLoop是一個(gè)用于循環(huán)監(jiān)聽和處理事件或者消息的模型,接收請求,然后派發(fā)給相關(guān)的處理模塊,wikipedia上有更為全面的介紹:Event_loop
- Cocoa RunLoop屬于Event Loop模型在Mac平臺的具體實(shí)現(xiàn)
- 其他平臺的類似實(shí)現(xiàn):X Window程序,Windows程序 ,Glib庫等
微觀上: Cocoa RunLoop
- Cocoa RunLoop本質(zhì)上就是一個(gè)對象,提供一個(gè)入口函數(shù)啟動(dòng)事件循環(huán),在滿足特點(diǎn)條件后才會退出。
- Cocoa RunLoop與普通while/for循環(huán)不同的是它能監(jiān)聽處理事件和消息,能智能休眠和被喚醒,這些功能的其實(shí)現(xiàn)依賴于Mac Port。
二、 Cocoa RunLoop的內(nèi)部結(jié)構(gòu)
但凡說到Cocoa RunLoop內(nèi)部結(jié)構(gòu),都離不開下面這張圖,來源于Apple開發(fā)者文檔

結(jié)合上圖,可將RunLoop架構(gòu)劃分為四個(gè)部分:
- 事件源
- 運(yùn)行模式
- 循環(huán)機(jī)制
- 執(zhí)行反饋
1. 事件源
Cocoa RunLoop接受的事件源分為兩種類型:Input Sources 和 Timer Sources
1.1. Input Sources
Input Sources通過異步派發(fā)的方式將事件轉(zhuǎn)送到目標(biāo)線程,事件類別分為兩大塊:
-
Port-Based Sources :
基于Mach端口的事件源,Cocoa和Core Foundation這兩個(gè)框架已經(jīng)提供了內(nèi)部支持,只需要調(diào)用端口相關(guān)的對象或者函數(shù)就能提供端口進(jìn)行通信。比如:將NSPort對象部署到RunLoop中,實(shí)現(xiàn)兩個(gè)線程的循環(huán)通信。
-
Custom Input Sources :
用戶自定義的輸入源:使用Core Foundation框架中CFRunLoopSourceRef對象的相關(guān)函數(shù)實(shí)現(xiàn)。具體實(shí)現(xiàn)可以查看另外一篇博客:Cocoa RunLoop 系列之Configure Custom InputSource
-
Cocoa Perform Selector Sources:Cocoa框架內(nèi)部實(shí)現(xiàn)的自定義輸入源,可以跨線程調(diào)用,實(shí)現(xiàn)線程見通信,有點(diǎn)類似于Port-Based事件源,不同的是這種事件源只在RunLoop上部署一次,執(zhí)行結(jié)束后便會自動(dòng)移除。如果目標(biāo)線程中沒有啟動(dòng)RunLoop也就意味著無法部署這類事件源,因此不會得到預(yù)期的結(jié)果。
使用Cocoa自定義事件源的函數(shù)接口,如下:
//部署在主線程
//參數(shù)列表:Selector:事件源處理函數(shù),Selector參數(shù),是否阻塞當(dāng)前線程,指定RunLoop模式
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
//部署在指定線程
//參數(shù)列表:Selector:事件源處理函數(shù),指定線程,Selector參數(shù),是否阻塞當(dāng)前線程,指定RunLoop模式
permSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
//部署在當(dāng)前線程
//參數(shù)列表:Selector:事件源處理函數(shù),Selector參數(shù),延時(shí)執(zhí)行時(shí)間,指定RunLoop模式
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
//撤銷某個(gè)對象通過函數(shù)performSelector:withObject:afterDelay:部署在當(dāng)前線程的全部或者指定事件源
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:
綜上,Input Sources包括基于Mach端口的事件源和自定義的事件源,二者的唯一區(qū)別在于被觸發(fā)的方式:前者是由內(nèi)核自動(dòng)觸發(fā),后者則需要在其他線程中手動(dòng)觸發(fā)。
1.2. Timer Sources
不同于Input Sources的異步派發(fā),Timer Source是通過同步派發(fā)的方式,在預(yù)設(shè)時(shí)間到達(dá)時(shí)將事件轉(zhuǎn)送到目標(biāo)線程。這種事件源可用于線程的自我提醒功能,實(shí)現(xiàn)周期性的任務(wù)。
- 如果RunLoop當(dāng)前運(yùn)行模式?jīng)]有添加Time Sources,則在RunLoop中部署的定時(shí)器不會被執(zhí)行。
- 設(shè)定的間隔時(shí)間與真實(shí)的觸發(fā)時(shí)間之間沒有必然聯(lián)系,定時(shí)器會根據(jù)設(shè)定的間隔時(shí)間周期性的派發(fā)消息到RunLoop,但是真實(shí)的觸發(fā)時(shí)間由RunLoop決定,假設(shè)RunLoop當(dāng)前正在處理其一個(gè)長時(shí)間的任務(wù),則觸發(fā)時(shí)間會被延遲,如果在最終觸發(fā)之前Timer已經(jīng)派發(fā)了N個(gè)消息,RunLoop也只會當(dāng)做一次派發(fā)對待,觸發(fā)一次對應(yīng)的處理函數(shù)。
2. 運(yùn)行模式
運(yùn)行模式類似于一個(gè)過濾器,用于屏蔽那些不關(guān)心的事件源,讓RunLoop專注于監(jiān)聽和處理指定的事件源和RunLoop Observer。
CFRunLoopMode 和 CFRunLoop 的數(shù)據(jù)結(jié)構(gòu)大致如下:
struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
...
};
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};
結(jié)合以上源碼,總結(jié)以下幾點(diǎn):
- 每種模式通過name屬性作為標(biāo)識。
- 一種運(yùn)行模式(Run Loop Mode)就是一個(gè)集合,包含需要監(jiān)聽的事件源Input Sources和Timer Soueces以及需要觸發(fā)的RunLoop observers。
- Cocoa RunLoop包含若干個(gè)Mode,調(diào)用RunLoop是指定的Mode稱之為CurrentMode。RunLoop可以在不同的Mode下切換,切換時(shí)退出CurrentMode,并保存相關(guān)上下文,再進(jìn)入新的Mode。
- 在啟動(dòng)Cocoa RunLoop是必須指定一種的運(yùn)行模式,且如果指定的運(yùn)行模式?jīng)]有包含事件源或者observers,RunLoop會立刻退出。
- CFRunLoop結(jié)構(gòu)中的commonModes是Mode集合,將某個(gè)Mode的name添加到commonModes集合中,表示這個(gè)Mode具有“common”屬性。
- CFRunLoop結(jié)構(gòu)中的commonModeItems則是共用源的集合,包括事件源和執(zhí)行反饋。這些共用源會被自動(dòng)添加到具有“common”屬性的Mode中。
** Note ** : 不同的運(yùn)行模式區(qū)別在于事件源的不同,比如來源于不同端口的事件和端口事件與Timer事件。不能用于區(qū)分不同的事件類型,比如鼠標(biāo)消息事件和鍵盤消息事件,因?yàn)檫@兩種事件都屬于基于端口的事件源。
以下是蘋果預(yù)定義好的一些運(yùn)行模式:
- NSDefaultRunLoopMode //默認(rèn)的運(yùn)行模式,適用于大部分情況
- NSConnectionReplyMode //Cocoa庫用于監(jiān)聽NSConnection對象響應(yīng),開發(fā)者很少使用
- NSModalPanelRunLoopMode //模態(tài)窗口相關(guān)事件源
- NSEventTrackingRunLoopMode //鼠標(biāo)拖拽或者屏幕滾動(dòng)時(shí)的事件源
- NSRunLoopCommonModes //用于操作RunLoop結(jié)構(gòu)中commonModes和commonModeItems兩個(gè)屬性
3. 循環(huán)機(jī)制
循環(huán)機(jī)制涉及兩方面:
3.1. RunLoop與線程之間的關(guān)系
Apple文檔中提到:開發(fā)者不需要手動(dòng)創(chuàng)建RunLoop對象,每個(gè)線程包括主線程都關(guān)聯(lián)了一個(gè)RunLoop對象。除了主線程的RunLoop在程序啟動(dòng)時(shí)被開啟,其他線程的RunLoop都需要手動(dòng)開啟。
待解決的疑問:
- 線程中的RunLoop是一直存在還是需要時(shí)再創(chuàng)建?
- 線程與RunLoop的是如何建立聯(lián)系的?
- 線程與RunLoop對象是否是一一對應(yīng)的關(guān)系?
3.2. RunLoop事件處理流程
弄清楚RunLoop內(nèi)部處理邏輯是理解RunLoop的關(guān)鍵,將單獨(dú)寫一篇博客進(jìn)行分析。
待解決的疑問:
- RunLoop如何處理不同事件源?
- RunLoop不同模式切換是如何實(shí)現(xiàn)的?
以上兩方面,將在下一篇博客Cocoa RunLoop 系列之源碼解析中結(jié)合源代碼來找到答案。
4. 執(zhí)行反饋
RunLoop Observers機(jī)制屬于RunLoop一個(gè)反饋機(jī)制,將RunLoop一次循環(huán)劃分成若干個(gè)節(jié)點(diǎn),當(dāng)執(zhí)行到對應(yīng)的節(jié)點(diǎn)調(diào)用相應(yīng)的回調(diào)函數(shù),將RunLoop當(dāng)前的執(zhí)行狀態(tài)反饋給用戶。
用戶可以通過Core Foundation框架中的CFRunLoopObserverRef注冊 observers。
-
監(jiān)聽節(jié)點(diǎn):
- The entrance to the run loop. //RunLoop啟動(dòng)
- When the run loop is about to process a timer. //即將處理Timer事件源
- When the run loop is about to process an input source. //即將處理Input事件源
- When the run loop is about to go to sleep. //即將進(jìn)入休眠
- When the run loop has woken up, but before it has processed the event that woke it up. //重新被喚醒,且在處理喚醒事件之前
- The exit from the run loop. //退出RunLoop
監(jiān)聽類別分為兩種:一次性和重復(fù)監(jiān)聽。
三、何時(shí)使用RunLoop
由于主線程的RunLoop在程序啟動(dòng)時(shí)被自動(dòng)創(chuàng)建并執(zhí)行,因此只有在其他線程中才需要手動(dòng)啟動(dòng)RunLoop。很多情況下,對于RunLoop的使用多數(shù)情況是在主線程中,包括進(jìn)行RunLoop模式切換,設(shè)置RunLoop Observer等。
在非主線程中,以下幾種情況適用于RunLoop:
- 使用基于端口或者自定義的事件源與其他線程進(jìn)行通信。
- 需要在當(dāng)前線程中使用Timer,必須部署才RunLoop中才有效。
- 在目標(biāo)線程中調(diào)用performSelector… 函數(shù),因?yàn)楸举|(zhì)上使用了Cocoa自定義的事件源,依賴于RunLoop才能被觸發(fā)。
- 線程需要進(jìn)行周期性的任務(wù),需要長時(shí)間存在,而非執(zhí)行一次。
四、總結(jié)
一直以來,RunLoop對我來說都屬于一個(gè)比較模糊的概念,在實(shí)際編程中也有用到RunLoop的一些功能,確實(shí)感覺到很強(qiáng)大,但是僅僅停留在應(yīng)用層面,并不是很理解具體含義。因此,為了更好的使用RunLoop,有必要研究和梳理RunLoop相關(guān)的知識點(diǎn)。