RunLoop深度探究(四)

我的博客地址:http://yangchao0033.github.io/blog/2016/01/08/runloopshen-du-tan-jiu-(4)/

譯文原文鏈接:Run Loops

Run loops 是與線程相關(guān)聯(lián)的基礎(chǔ)設(shè)施的一部分。Run loop 是用來(lái)調(diào)度工作并且協(xié)調(diào)傳入事件的時(shí)間處理循環(huán)。run loop 的目的是:讓你的線程在有工作的任務(wù)的時(shí)候保持忙碌,并且在空閑的時(shí)候使線程保持休眠。

Run loop 的管理并不是完全自動(dòng)的,你仍然需要設(shè)計(jì)你的線程代碼,并利用這些代碼在合適的時(shí)機(jī)開啟 run loop 并且相應(yīng)傳入的事件。Cocoa 和 Core Foundation 框架都提供了run loop 對(duì)象,以便于幫助你配置和管理你的線程 run loop。你的應(yīng)用不需要顯式的創(chuàng)建這些 run loop 對(duì)象。每條線程,包括應(yīng)用的主線程,都會(huì)配有一個(gè)相關(guān)聯(lián)的 run loop 對(duì)象。只有子線程們需要顯式的(手動(dòng))運(yùn)行它們的 run loop。然而,該 app 框架將會(huì)自動(dòng)設(shè)置并且運(yùn)行作為應(yīng)用程序啟動(dòng)過程的一部分的處于 main thread (主線程)的 run loop。

接下來(lái),我們一起看看關(guān)于 run loop 以及它們的配置相關(guān)的更多信息。參照NSRunLoop Class Reference 以及 CFRunLoop Reference。

Run Loop 剖析

run loop 是一個(gè)名副其實(shí)的循環(huán)。他會(huì)是你的線程不停地循環(huán)工作,具體包括線程的進(jìn)入并且用來(lái)響應(yīng)輸入源所運(yùn)行的事件處理程序。你的代碼提供的控制狀態(tài)將會(huì)被用來(lái)實(shí)現(xiàn)run loop具體的循環(huán)部分。換句話說,你的代碼需要提供 while 或 for 循環(huán)來(lái)驅(qū)動(dòng)你的 run loop。在你的 run loop 中,你使用一個(gè) run loop 對(duì)象去“執(zhí)行”接收事件的事件處理代碼和調(diào)用已經(jīng)安裝好的 handlers。

run loop 通過兩種不同類型的 source 來(lái)接收事件。Input source 傳遞異步事件,通常包括從其他線程或者其他應(yīng)用發(fā)來(lái)的 message 。Timer source 也傳遞異步事件,通常發(fā)生在一個(gè)被安排好的時(shí)間或者重復(fù)的時(shí)間間隔。這兩種類型的 source 都會(huì)使用 application-specific 處理例程來(lái)處理到來(lái)的事件。

插圖3-1展示了 run loop 各種 source 的概念結(jié)構(gòu)。input source 傳遞異步事件到相對(duì)應(yīng)的 handler(處理程序)并且引起 runUtilDate: 方法(調(diào)用線程相關(guān)的 NSRunLoop 對(duì)象)退出。

插圖3-1 run loop 和它的 source 結(jié)構(gòu)

插圖3-1
插圖3-1

除了處理 input source,run loop 同樣也會(huì)生成一些關(guān)于 run loop 的行為的通知。注冊(cè) run-loop observers(觀察者)去接收這些通知并且使用他們?nèi)ピ诰€程上做額外的處理。你可以使用 Core Foundation 框架去在你的線程上安裝 run-loop observer 。

下面這些段落提供了更多地關(guān)于 runloop的組成 和 它們可以操作的modes,它們還描述了在事件處理的不同時(shí)間點(diǎn)生成的通知。

Run Loop Modes

一個(gè) run loop mode 是一個(gè)包含 input sources 和 被監(jiān)視的 timers 和 將要被通知的 run-loop 的observer 所組成的集合。每次你啟動(dòng)你的 run loop 時(shí),你都會(huì)指定(隱式或者顯式的)一種特定的“mode”來(lái)運(yùn)行。在執(zhí)行該運(yùn)行循環(huán)的期間,只有與指定運(yùn)行的 mode 相關(guān)聯(lián)的 source 才會(huì)被檢測(cè)和允許去發(fā)送他們的事件。(相同的,只有與指定運(yùn)行的 mode 相關(guān)聯(lián)的 observer 會(huì)被 run loop 的進(jìn)度(progress)所通知)。關(guān)聯(lián)到其他 modes 的 sources 直到 run loop 切換到對(duì)應(yīng)的 mode 時(shí)才會(huì)繼續(xù)讓新的事件通行(只有到對(duì)應(yīng)的mode下,相關(guān)的 source 才會(huì)繼續(xù)其對(duì)應(yīng)的功能)。

在你的代碼中,可以使用名字來(lái)標(biāo)記 modes,包括 Cocoa 和 Core Foundation 都定義了一些默認(rèn)的mode和一些常用的mode,代碼中你可以通過字符串來(lái)區(qū)別這些mode,你可以定義自定義的mode,僅僅通過指定簡(jiǎn)單的字符串來(lái)標(biāo)記這些自定義的mode。盡管可以隨意的對(duì) mode 名字進(jìn)行復(fù)制,但是這些 mode 的內(nèi)容卻不行。你必須確保你添加到得任何你創(chuàng)建的 mode 中的一個(gè)或多個(gè)input sources, timers, 或者 run-loop observer都是有用的。

你使用 modes 并通過你的 run loop 特定的掃描中過濾掉干擾的事件。大多數(shù)情況下,你都會(huì)將你的 run loop 運(yùn)行在系統(tǒng)默認(rèn)的 mode 中。模態(tài)面板可能會(huì)運(yùn)行在“ modal ” mode 中。當(dāng)處于這種 mode 下,只有和 source 關(guān)聯(lián)的模態(tài)面板才可以發(fā)送事件到線程中去。對(duì)于子線程來(lái)說,你可能會(huì)使用自定義 modes 去防止優(yōu)先級(jí)抵的 source 在時(shí)間要求嚴(yán)格的操作中發(fā)送事件。</br>
Note: modes 的區(qū)別取決于 event(事件) 的 source ,不是 event 的類型。比如,你可能不會(huì)用 mode 去僅僅匹配一個(gè)鼠標(biāo)點(diǎn)擊事件或者鍵盤點(diǎn)擊事件。 你可能會(huì)用 mode 去監(jiān)聽一組不同的端口。暫停 timer ,或者改變當(dāng)前監(jiān)視的 source ,然后,run loop observer 會(huì)立即開始監(jiān)視。
</br>
列表3-1列出了 Cocoa 和 Core Foundation 框架中你可以使用的具有官方文檔描述的一些標(biāo)準(zhǔn)的 mode,列名稱列出了你在代碼中需要指定 mode 時(shí)需要使用的實(shí)際常量。

Table 3-1 預(yù)定義的 run loop modes

Mode Name Description
Default NSDefaultRunLoopMode (Cocoa)</br>kCFRunLoopDefaultMode (Core Foundation) Default Mode 是用的最多的 mode 操作。在大多數(shù)情況下,你應(yīng)該使用此模式啟動(dòng) Run Loop 和配置您的 input source。
Connection NSConnectionReplyMode(Cocoa) Cocoa 使用這個(gè) mode 來(lái) NSConnection 對(duì)象共同監(jiān)視 replies (回復(fù)響應(yīng)),你一般不需要自己使用該 mode
Modal NSModalPanelRunLoopMode(Cocoa) Cocoa 使用這個(gè) mode 來(lái)標(biāo)記確認(rèn)用于 modal panel(面板、控制器)的事件
Event tracking NSEventTrackingRunLoopMode(Cocoa) Cocoa 使用這個(gè) mode 來(lái)約束鼠標(biāo)拖動(dòng)循環(huán)和其他類型的用戶界面跟蹤循環(huán)的輸入事件
Common modes NSRunLoopCommonModes(Cocoa)</br>kCFRunLoopCommonModes (Core Foundation) 這是一個(gè) common mode 的可配置組,用該 mode 關(guān)聯(lián)的某個(gè) input source 同時(shí)也被組中的這些 mode 所關(guān)聯(lián)。對(duì)于 Cocoa 的應(yīng)用,這一套包括 default modal, 和 默認(rèn)的 event tracking 這些 mode。Core Foundation 只包括開始的 default mode。你可以使用CFRunLoopAddCommonMode function(函數(shù))來(lái)添加自定義的 mode 到 mode 組中。

Input Sources

input source 異步傳遞事件到你的線程中。event 的source 取決于 input source 的類型,通常是兩個(gè)類型中的一個(gè)。基于 port 的 input source 監(jiān)視著你的應(yīng)用中的 Mach Port 。自定義的 input source 監(jiān)視著你的自定義的事件 source 。至于你的run loop 來(lái)講,他不應(yīng)該在是否是基于 port 的 input source 還是 自定義 input source 出現(xiàn)問題。系統(tǒng)通常會(huì)實(shí)現(xiàn)這兩種全部允許程序員自己使用類型的 input source 。這兩種 source 之間唯一的不同在于它們是如何被發(fā)送信號(hào)的。基于 port 的 source 可以通過 kernel 內(nèi)核接收發(fā)送的信號(hào),而自定義的 source 必須手動(dòng)從另一個(gè)線程發(fā)送信號(hào)過來(lái)。

當(dāng)你創(chuàng)建一個(gè) input source 時(shí),你可以給它分配一個(gè)或多個(gè)你的 run loop 的 mode 。受 mode 影響的 input source 會(huì)在任何給定的時(shí)刻受到監(jiān)視,在大多數(shù)情況下,你的 run loop 會(huì)運(yùn)行在 default mode 下,但是你也可以自己指派自定義的mode給它。如果一個(gè) input source 沒有處于當(dāng)前受監(jiān)視的 mode 下,那么任何它產(chǎn)生的事件都會(huì)被掛起,除非你的 run loop 在正確的 mode 下運(yùn)行(就是讓你的 run loop 以你需要監(jiān)視的 input source 所屬的 mode 開始運(yùn)行,這樣 run loop 才能監(jiān)視到你的 input source 產(chǎn)生的事件)。

下面介紹一些 input source 。。。

基于 port 的 source

Cocoa 和 Core Foundation 框架都提供了內(nèi)置的支持--使用 port 相關(guān)的對(duì)象和函數(shù)來(lái)創(chuàng)建基于 port 的input source。例如,在 Cocoa 中,你根本不用自己直接創(chuàng)建一個(gè) input source,你只需要?jiǎng)?chuàng)建一個(gè) port 對(duì)象,并使用 NSPort 的方法去將 port 添加到 run loop 中去。port 對(duì)象會(huì)為你處理好創(chuàng)建和配置一個(gè)你需要的 input source 這些底層的事情。

在 Core Foundation 中, 你必須手動(dòng)的兩者都創(chuàng)建,其中包括 port 和 它的 run loop source ,在兩種情況下, 你可以使用 port 不透明類型的 (CFMachPortRef, CFMessagePortRef, or CFSocketRef) 相關(guān)的函數(shù)來(lái)創(chuàng)建合適的對(duì)象(you use the functions associated with the port opaque type (CFMachPortRef, CFMessagePortRef, or CFSocketRef) to create the appropriate objects)。

如何創(chuàng)建和配置自定義的基于 port 的源,可以參照創(chuàng)建一個(gè)基于 port 的 input source
</br>
</br>

自定義 input source

為了自定義一個(gè) input source,你必須使用 Core Foundation 框架中 CFRunLoopSourceRef的不透明類型相關(guān)聯(lián)的函數(shù)。你可以使用多個(gè)回調(diào)函數(shù)來(lái)配置你的自定義 input source ,Core Foundation 框架會(huì)配置你的 source 在不同的點(diǎn)去調(diào)用這些函數(shù),處理將要發(fā)生的事件,并在 source 被移出 run loop 的時(shí)候銷毀 source。

除了定義在事件到來(lái)時(shí)自定義 source 的行為,你還必須定義事件傳遞機(jī)制。source 的這部分運(yùn)行在一個(gè)單獨(dú)的線程中,并且負(fù)責(zé) 當(dāng)數(shù)據(jù)已經(jīng)準(zhǔn)備好去被處理的時(shí)候,這部分會(huì)去提供一個(gè)擁有數(shù)據(jù)的 input source,并且發(fā)信號(hào)給 input source。這個(gè)事件傳遞機(jī)制取決于你,但是也不用過于復(fù)雜。

有關(guān)如何創(chuàng)建一個(gè)自定義 input source 的例子,可以參考定義一個(gè)自定義 input source, 關(guān)于自定義 input source 的詳細(xì)信息, 可以參考 CFRunLoopSourceReference

Run Loop 的時(shí)間執(zhí)行次序

每一次你運(yùn)行 run loop 時(shí),你的線程的 run loop 都會(huì)處理未完成的事件,并且為已經(jīng)注冊(cè)的任何觀察者生成通知信息。下面是它執(zhí)行的具體步驟:

  1. 通知觀察者 runloop 已經(jīng)進(jìn)入。
  • 通知觀察者 任何就緒的 timer 即將觸發(fā)。
  • 通知觀察者 任何 不是基于端口的 input source (source0)即將觸發(fā)
  • 觸發(fā)任何不是基于端口的并且準(zhǔn)備就緒的 input source。
  • 假如存在已經(jīng)準(zhǔn)備好的基于端口的 input source (source1),將會(huì)等待被觸發(fā)。并立即開始處理事件。跳轉(zhuǎn)至步驟 9 。
  • 通知觀察者 線程即將睡眠。
  • 設(shè)置讓線程在符合如下條件之一時(shí)從睡眠狀態(tài)喚醒:
    • 一個(gè)基于端口的 input source(source1)產(chǎn)生的事件到來(lái)。
    • timer 觸發(fā)了。
    • 超出 runloop 預(yù)定的時(shí)間。
    • run loop 明確喚醒
  • 通知觀察者,線程剛剛喚醒。
  • 處理剩余的事件:
    • 如果一個(gè)用戶定義的 timer 觸發(fā),處理 timer 的事件并且重啟 ru loop ,跳轉(zhuǎn)值步驟 2 。
    • 如果一個(gè) input source 觸發(fā),傳遞這個(gè)事件。
    • run loop 已經(jīng)明確地被喚醒但是還沒有超時(shí)。重啟 run loop,并且跳轉(zhuǎn)至 第二步。
  • 通知觀察者,run loop 已經(jīng)退出。
    因?yàn)?timer 和 input source 發(fā)出的 observer 通知是在這些事件確實(shí)是發(fā)生時(shí)被發(fā)送的,那么在事件發(fā)生的時(shí)間和 observer 收到的通知的時(shí)間會(huì)存在時(shí)間間隙。如果對(duì)事件發(fā)生時(shí)間的準(zhǔn)確性要求非常嚴(yán)格,那么你需要采取 睡眠喚醒睡眠 的通知方式來(lái)幫助你糾正事件實(shí)際發(fā)生的時(shí)間。

由于定時(shí)器等周期性事件是在 run loop 運(yùn)行的時(shí)候發(fā)送的,需要避免在發(fā)送這些事件時(shí)被打斷。典型的例子比如:只要進(jìn)入運(yùn)行循環(huán),并且反復(fù)實(shí)時(shí)地從應(yīng)用中請(qǐng)求事件來(lái)實(shí)現(xiàn)對(duì)鼠標(biāo)跟蹤的常規(guī)動(dòng)作。由于你的代碼是直接抓取事件的,而不是像平時(shí)一樣讓應(yīng)用分發(fā)這些事件的,所以 timer 是不能被觸發(fā)的,除非鼠標(biāo)追蹤程序退出并返回到應(yīng)用程序控制。

一個(gè) run loop 可以使用 run loop 對(duì)象顯示喚醒,其他的事件可能也會(huì)引起 run loop 的喚醒。比如,添加另一個(gè) 不是基于端口的 input source(source1) 喚醒 run loop 從而使 input source 可以被立即處理(相比于一直處于等待狀態(tài)直到其他時(shí)間發(fā)生才喚醒)。

在什么時(shí)候適合使用 RunLoop ?

唯一需要你顯示的使用 run loop 的時(shí)候是:當(dāng)你為你的應(yīng)用創(chuàng)建了子線程的時(shí)候。在你的應(yīng)用中,主線程的 run loop 是基礎(chǔ)設(shè)施中至關(guān)重要的一部分。所以,app 的框架都會(huì)提供運(yùn)行主線程 loop 的代碼并且自動(dòng)開啟 loop。iOS中UIApplication(或者OSX中NSApplication)的run方法會(huì)開啟一個(gè)應(yīng)用中的主 loop 作為應(yīng)用程序啟動(dòng)步驟的一部分。如果你使用 Xcode 的項(xiàng)目模板來(lái)創(chuàng)建你的應(yīng)用,你完全不用自已去顯示的調(diào)用這些常規(guī)方法。

對(duì)子線程來(lái)講,你需要決定是否一個(gè) run loop 對(duì)它來(lái)說是必要的,如果是,需要你自己配置并且啟動(dòng)它。你并不需要在任何情況下都開啟線程的 run loop。比如:你使用線程去執(zhí)行一些長(zhǎng)期的并且預(yù)先決定的任務(wù)的時(shí)候,你可能需要避免開啟 run loop 。Run loop 通常服務(wù)于一些需要你和線程之間互動(dòng)性更強(qiáng)的特殊場(chǎng)景。比如,你需要在以下這些場(chǎng)景中開啟你的 run loop:

  • 使用 port 或者自定義 input source 來(lái)和其他線程進(jìn)行通信。
  • 在線程中使用 timer 。
  • 在 Cocoa 的應(yīng)用中使用任何與 performSelector…相關(guān)的方法。
  • 讓你的線程繼續(xù)執(zhí)行周期性的任務(wù)。

如果你選擇使用 run loop,配置和建立它是非常簡(jiǎn)單的。如同所有的多線程編程一樣,你需要有計(jì)劃的在合適的情況下退出子線程。讓它(run loop)更好的退出而不是迫使它終止永遠(yuǎn)都是更好的更干凈的結(jié)束線程的方法。關(guān)于如何配置并且退出的方法將在使用Run Loop對(duì)象中向大家展示。

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

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

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