Cocoa RunLoop 系列之基礎(chǔ)知識

博客地址

這篇博客主要結(jié)合Apple開發(fā)者文檔和個(gè)人的理解,寫的一篇關(guān)于Cocoa RunLoop基本知識點(diǎn)的文章。在文檔的基礎(chǔ)上,概況和梳理了RunLoop相關(guān)的知識點(diǎn)。

一、Event Loop & Cocoa RunLoop

宏觀上:Event Loop

  1. RunLoop是一個(gè)用于循環(huán)監(jiān)聽和處理事件或者消息的模型,接收請求,然后派發(fā)給相關(guān)的處理模塊,wikipedia上有更為全面的介紹:Event_loop
  2. Cocoa RunLoop屬于Event Loop模型在Mac平臺的具體實(shí)現(xiàn)
  3. 其他平臺的類似實(shí)現(xiàn):X Window程序,Windows程序 ,Glib庫等

微觀上: Cocoa RunLoop

  1. Cocoa RunLoop本質(zhì)上就是一個(gè)對象,提供一個(gè)入口函數(shù)啟動(dòng)事件循環(huán),在滿足特點(diǎn)條件后才會退出。
  2. 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ā)者文檔

圖1-1 RunLoop結(jié)構(gòu)圖

結(jié)合上圖,可將RunLoop架構(gòu)劃分為四個(gè)部分:

  1. 事件源
  2. 運(yùn)行模式
  3. 循環(huán)機(jī)制
  4. 執(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)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容