這篇博客主要結合Apple開發者文檔和個人的理解,寫的一篇關于Cocoa RunLoop基本知識點的文章。在文檔的基礎上,概況和梳理了RunLoop相關的知識點。
一、Event Loop & Cocoa RunLoop
宏觀上:Event Loop
- RunLoop是一個用于循環監聽和處理事件或者消息的模型,接收請求,然后派發給相關的處理模塊,wikipedia上有更為全面的介紹:Event_loop
- Cocoa RunLoop屬于Event Loop模型在Mac平臺的具體實現
- 其他平臺的類似實現:X Window程序,Windows程序 ,Glib庫等
微觀上: Cocoa RunLoop
- Cocoa RunLoop本質上就是一個對象,提供一個入口函數啟動事件循環,在滿足特點條件后才會退出。
- Cocoa RunLoop與普通while/for循環不同的是它能監聽處理事件和消息,能智能休眠和被喚醒,這些功能的其實現依賴于Mac Port。
二、 Cocoa RunLoop的內部結構
但凡說到Cocoa RunLoop內部結構,都離不開下面這張圖,來源于Apple開發者文檔
結合上圖,可將RunLoop架構劃分為四個部分:
- 事件源
- 運行模式
- 循環機制
- 執行反饋
1. 事件源
Cocoa RunLoop接受的事件源分為兩種類型:Input Sources 和 Timer Sources
1.1. Input Sources
Input Sources通過異步派發的方式將事件轉送到目標線程,事件類別分為兩大塊:
-
Port-Based Sources :
基于Mach端口的事件源,Cocoa和Core Foundation這兩個框架已經提供了內部支持,只需要調用端口相關的對象或者函數就能提供端口進行通信。比如:將NSPort對象部署到RunLoop中,實現兩個線程的循環通信。
-
Custom Input Sources :
用戶自定義的輸入源:使用Core Foundation框架中CFRunLoopSourceRef對象的相關函數實現。具體實現可以查看另外一篇博客:Cocoa RunLoop 系列之Configure Custom InputSource
-
Cocoa Perform Selector Sources:Cocoa框架內部實現的自定義輸入源,可以跨線程調用,實現線程見通信,有點類似于Port-Based事件源,不同的是這種事件源只在RunLoop上部署一次,執行結束后便會自動移除。如果目標線程中沒有啟動RunLoop也就意味著無法部署這類事件源,因此不會得到預期的結果。
使用Cocoa自定義事件源的函數接口,如下:
//部署在主線程
//參數列表:Selector:事件源處理函數,Selector參數,是否阻塞當前線程,指定RunLoop模式
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
//部署在指定線程
//參數列表:Selector:事件源處理函數,指定線程,Selector參數,是否阻塞當前線程,指定RunLoop模式
permSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
//部署在當前線程
//參數列表:Selector:事件源處理函數,Selector參數,延時執行時間,指定RunLoop模式
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
//撤銷某個對象通過函數performSelector:withObject:afterDelay:部署在當前線程的全部或者指定事件源
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:
綜上,Input Sources包括基于Mach端口的事件源和自定義的事件源,二者的唯一區別在于被觸發的方式:前者是由內核自動觸發,后者則需要在其他線程中手動觸發。
1.2. Timer Sources
不同于Input Sources的異步派發,Timer Source是通過同步派發的方式,在預設時間到達時將事件轉送到目標線程。這種事件源可用于線程的自我提醒功能,實現周期性的任務。
- 如果RunLoop當前運行模式沒有添加Time Sources,則在RunLoop中部署的定時器不會被執行。
- 設定的間隔時間與真實的觸發時間之間沒有必然聯系,定時器會根據設定的間隔時間周期性的派發消息到RunLoop,但是真實的觸發時間由RunLoop決定,假設RunLoop當前正在處理其一個長時間的任務,則觸發時間會被延遲,如果在最終觸發之前Timer已經派發了N個消息,RunLoop也只會當做一次派發對待,觸發一次對應的處理函數。
2. 運行模式
運行模式類似于一個過濾器,用于屏蔽那些不關心的事件源,讓RunLoop專注于監聽和處理指定的事件源和RunLoop Observer。
CFRunLoopMode 和 CFRunLoop 的數據結構大致如下:
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
...
};
結合以上源碼,總結以下幾點:
- 每種模式通過name屬性作為標識。
- 一種運行模式(Run Loop Mode)就是一個集合,包含需要監聽的事件源Input Sources和Timer Soueces以及需要觸發的RunLoop observers。
- Cocoa RunLoop包含若干個Mode,調用RunLoop是指定的Mode稱之為CurrentMode。RunLoop可以在不同的Mode下切換,切換時退出CurrentMode,并保存相關上下文,再進入新的Mode。
- 在啟動Cocoa RunLoop是必須指定一種的運行模式,且如果指定的運行模式沒有包含事件源或者observers,RunLoop會立刻退出。
- CFRunLoop結構中的commonModes是Mode集合,將某個Mode的name添加到commonModes集合中,表示這個Mode具有“common”屬性。
- CFRunLoop結構中的commonModeItems則是共用源的集合,包括事件源和執行反饋。這些共用源會被自動添加到具有“common”屬性的Mode中。
** Note ** : 不同的運行模式區別在于事件源的不同,比如來源于不同端口的事件和端口事件與Timer事件。不能用于區分不同的事件類型,比如鼠標消息事件和鍵盤消息事件,因為這兩種事件都屬于基于端口的事件源。
以下是蘋果預定義好的一些運行模式:
- NSDefaultRunLoopMode //默認的運行模式,適用于大部分情況
- NSConnectionReplyMode //Cocoa庫用于監聽NSConnection對象響應,開發者很少使用
- NSModalPanelRunLoopMode //模態窗口相關事件源
- NSEventTrackingRunLoopMode //鼠標拖拽或者屏幕滾動時的事件源
- NSRunLoopCommonModes //用于操作RunLoop結構中commonModes和commonModeItems兩個屬性
3. 循環機制
循環機制涉及兩方面:
3.1. RunLoop與線程之間的關系
Apple文檔中提到:開發者不需要手動創建RunLoop對象,每個線程包括主線程都關聯了一個RunLoop對象。除了主線程的RunLoop在程序啟動時被開啟,其他線程的RunLoop都需要手動開啟。
待解決的疑問:
- 線程中的RunLoop是一直存在還是需要時再創建?
- 線程與RunLoop的是如何建立聯系的?
- 線程與RunLoop對象是否是一一對應的關系?
3.2. RunLoop事件處理流程
弄清楚RunLoop內部處理邏輯是理解RunLoop的關鍵,將單獨寫一篇博客進行分析。
待解決的疑問:
- RunLoop如何處理不同事件源?
- RunLoop不同模式切換是如何實現的?
以上兩方面,將在下一篇博客Cocoa RunLoop 系列之源碼解析中結合源代碼來找到答案。
4. 執行反饋
RunLoop Observers機制屬于RunLoop一個反饋機制,將RunLoop一次循環劃分成若干個節點,當執行到對應的節點調用相應的回調函數,將RunLoop當前的執行狀態反饋給用戶。
用戶可以通過Core Foundation框架中的CFRunLoopObserverRef注冊 observers。
-
監聽節點:
- The entrance to the run loop. //RunLoop啟動
- 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. //即將進入休眠
- 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
監聽類別分為兩種:一次性和重復監聽。
三、何時使用RunLoop
由于主線程的RunLoop在程序啟動時被自動創建并執行,因此只有在其他線程中才需要手動啟動RunLoop。很多情況下,對于RunLoop的使用多數情況是在主線程中,包括進行RunLoop模式切換,設置RunLoop Observer等。
在非主線程中,以下幾種情況適用于RunLoop:
- 使用基于端口或者自定義的事件源與其他線程進行通信。
- 需要在當前線程中使用Timer,必須部署才RunLoop中才有效。
- 在目標線程中調用performSelector… 函數,因為本質上使用了Cocoa自定義的事件源,依賴于RunLoop才能被觸發。
- 線程需要進行周期性的任務,需要長時間存在,而非執行一次。
四、總結
一直以來,RunLoop對我來說都屬于一個比較模糊的概念,在實際編程中也有用到RunLoop的一些功能,確實感覺到很強大,但是僅僅停留在應用層面,并不是很理解具體含義。因此,為了更好的使用RunLoop,有必要研究和梳理RunLoop相關的知識點。