[譯]iOS RunLoops

AlexCorleone同學發表於2018-03-07

翻譯來源: RunLoops

Run Loops

       RunLoops是與執行緒緊密相關的基礎架構的一部分,簡稱執行迴圈。RunLoop是一個事件處理迴圈,用於安排工作並協調接收到的事件。RunLoop的目的是在有任務的時候執行緒處於繁忙狀態(thread busy),並在沒有任務的時候執行緒處於休眠狀態(thread sleep)。

      RunLoop管理不是完全自動的。我們仍然需要設計執行緒的程式碼以便在適當的時間啟動RunLoop並響應傳入的事件。Cocoa和Core Foundation都提供了RunLoop物件,以幫助我們配置和管理執行緒的RunLoop。我們不需要明確的建立RunLoop物件;每一個執行緒(包含主執行緒在內的)都有一個與之關聯的RunLoop物件。但是,只有次執行緒需要顯式的執行RunLoop。作為應用程式啟動的一部分,應用程式框架自動設定並在主執行緒上執行RunLoop。

     下面將提供有關RunLoop的更多資訊以及如何為應用程式配置它們。有關RunLoop物件的更多資訊,請參考 NSRunLoop Class Reference and CFRunLoop Reference.

一、Run Loop解析

     RunLoop就像它的名字一樣是一個執行迴圈。在這個檢測迴圈內,使用與之繫結的執行緒來執行事件處理程式以響應傳入的事件。我們的程式碼用於實現RunLoop的實際迴圈部分的控制語句,換句話說,這部分程式碼將驅動RunLoop的while或者for迴圈。在迴圈中,使用RunLoop物件來“執行”接收事件並呼叫已安裝處理程式的程式碼處理事件。

     Run Loop接收兩種不同型別源的事件。輸入源(input sources)傳遞非同步事件,通常是來自另一個執行緒或者來自不同的應用程式的訊息。計時器源(Timer sources)提供發生在計劃時間或重複間隔的同步事件。這兩種型別的源都使用特定於應用程式的處理程式來處理事件到達時的狀態。

     圖(1-1)顯示了RunLoop和各種來源的概念結構。輸入源(input sources)將非同步事件傳遞給相應的處理程式,並導致runUntilDate:方法(線上程關聯的RunLoop物件上呼叫)退出。計時器源(Timer source)將事件傳遞到其處理程式例程,但不會導致RunLoop退出。

[譯]iOS RunLoops
                                          圖(1-1)執行迴圈及其來源的的結構

     除了處理輸入源(input sources)之外, Run Loop還會生成有關RunLoop行為的通知。已註冊的Run Loop觀察器可以接收這些通知並使用它們對執行緒進行附加處理。我們可以使用Core Foundtion 線上程上安裝Run Loop觀察器。

     下面的部分提供了關於RunLoop的元件和其執行模式的更多資訊。它們還描述了在處理事件期間在不同時間生成的通知。

1.1Run Loop的模式

     RunLoop的模式是要監視的輸入源和計時器的集合,以及要通知的RunLoop觀察器的集合。每次執行RunLoop時,都需要(顯式或隱式的)指定要執行的特定"模式"。在執行迴圈的過程中,只監視與該模式關聯的源並允許其傳送事件。(同樣,只有與該模式關聯的觀察器才會收到RunLoop程式的通知。)與其他模式相關的源儲存到任何新事件,直到隨後以適當的模式通過迴圈為止。

     在程式碼中,我們可以通過名字來識別模式。Cocoa和Core Foundation都定義了一個預設模式和幾種常用模式,以及用於在程式碼中指定這些模式的字串。我們可以通過為模式名稱指定自定義字串定義自定義模式。雖然分配給自定義模式的名稱是任意的,但這些模式的內容不是。因此必須確保將一個或者多個輸入源、計時器或者RunLoop觀察器新增到與我們建立的模式中,以便它們有用。

     在特定的RunLoop中使用模式可以過濾掉不需要的源中的事件。大多數情況下,我們需要在系統定義的“預設”模式下執行RunLoop。但是,模態皮膚可能會以“模態”模式執行。在此模式下,只有與模態皮膚相關的源才會將事件傳遞給執行緒。對於輔助執行緒,也可以使用自定義模式來防止低優先順序的源在時間關鍵型操作期間傳遞事件。

注意:模式基於事件的源進行區分,而不是事件的型別。例如,我們不會使用模式僅匹配滑鼠點選(mousedown)事件或僅匹配鍵盤事件。我們也可以使用模式監聽一組不同的埠,暫時暫停計時器,或者更改當前正在監視的原始碼和RunLoop觀察器。

下面列出了Cocoa和Core Foundation定義的標準模式以及何時使用該模式的描述。

模式1

model : Default

Name:NSDefaultRunLoopModel(Cocoa) kCFRunLoopDefaultMode(Core Foundation)

Description : 預設模式是用於大多數操作的模式。 大多數情況下, 我們應該使用此模式來啟動RunLoop並配置輸入源。

模式2:

model : Connection

Name: NSConnectionReplyModel(Cocoa)

Description : Cocoa將此模式與NSConnection物件一起使用來監聽響應。我們應該很少需要自己使用這種模式。

模式3:

model : Modal

Name:NSModelPanelRunLoopModels(Cocoa)

Description : Cocoa使用該模式識別用於模態皮膚的事件

模式4:

model : Event tracking

Name: NSEventTrackingRunLoopModel(Cocoa)

Description : Cocoa使用該模式來限制滑鼠拖動迴圈和其他型別的使用者介面跟蹤迴圈期間的傳入的事件。

模式5:

model : Common modes

Name:NSRunLoopCommonModes(Cocoa)kCFRunLoopCommonModes(Core Foundation)

Description : 這是一組可配置的常用模式組。將輸入源與此模式相關聯也會將其與組中的每一個模式相關聯。對於Cocoa應用程式,預設情況下,此集合包含預設、模式和事件跟蹤模式。Core Foundation最初只包含預設模式。我們也可以使用CFRunLoopAddCommonMode函式將自定義模式新增到集合。

1.2輸入源

     輸入源以非同步的方式向您的執行緒傳遞事件。事件的來源取決於輸入源的型別,它通常是兩類中的一類。基於埠的輸入源監視你的應用程式的Mach埠。自定義輸入源監視自定義事件源。就RunLoop而言,輸入源是基於埠的還是自定義的應該沒有關係。系統通常實現兩種型別的輸入源,我們可以按照原樣使用它們。兩個來源之間的唯一區別是他們如何發出訊號。基於埠的源由核心自動傳送訊號,自定義源必須從另一個執行緒手動傳送訊號。

     建立輸入源時,可以將其分配給RunLoop的一個或多個模式。模式會影響在特定的時刻監視哪些輸入源。大多數情況下,在預設模式下執行RunLoop,但也可以指定自定義模式。如果輸入源不處於當前監控的模式,則會生成其生成的所有事件,直到RunLoop以正確的模式執行。

下面是各個輸入源的介紹。

1.2.1.基於埠的源

     Cocoa和Core Foundation為使用與埠相關的物件和函式建立基於埠的輸入源提供了內建的支援。例如,在Cocoa中,我們不需要直接建立輸入源。而是隻需要建立一個埠物件並使用NSPort的方法將該埠新增到RunLoop。port物件為我們處理所需要輸入源的建立和配置。

     在Core Foundation中,我們必須手動建立埠及其RunLoop源。在這兩種情況下,都使用與埠不透明型別(CFMachPortRef,CFMessagePortRef或CFSocketRef)相關的函式來建立適當的物件。

有關如何設定和配置自定義的埠源的示例,請參閱3.3配置基於埠的輸入源

1.2.2.自定義輸入源

     要建立自定義的輸入源,我們必須使用與Core Foundation中的 CFRunLoopSourceRef 不透明型別關聯的函式。我們可以使用多個回撥函式來配置自定義的輸入源。Core Foundation在不同的點呼叫這些函式來配置原始碼,處理所有的傳入事件,並在原始碼從RunLoop中移除時刪除原始碼。

     除了在事件到達時定義自定義源的行為之外,還必須定義事件傳遞機制。這部分原始碼在單獨的執行緒上執行,負責為輸入源提供資料,並在資料準備好處理時用訊號通知它。事件傳遞機制取決於我們,但不必過於複雜。

有關如何建立自定義輸入源的示例,請參閱下文定義自定義的輸入源。有關自定義輸入源的參考資訊,請參閱CFRunLoopSource參考。

1.2.3.Cocoa執行選擇器源

     除了基於埠的原始碼之外,Cocoa還定義了一個自定義輸入源,允許我們在任何執行緒執行選擇器。與基於埠的源一樣,執行選擇器請求在目標執行緒上被序列化,從而減輕了在一個執行緒上執行多個方法時可能發生的許多同步問題。與基於埠的源不同,執行選擇器源在執行其選擇器後從RunLoop中移除。

注意:在OS X v10.5之前,執行選擇器源主要用於將訊息傳送到主執行緒,但在OS X v10.5及更高版本和iOS中,可以使用它們向任何執行緒傳送訊息。

     在另一個執行緒上執行選擇器時,目標執行緒必須具有活動的RunLoop。對於我們建立的執行緒,這意味著等待我們的程式碼明確的啟動RunLoop。但是,因為主執行緒的RunLoop是在應用程式呼叫應用程式的委託的applicationDidFinishLaunching:方法時自己啟動的,因此在該方法呼叫之後就可以開始在主執行緒上發出呼叫。RunLoop每次通過迴圈處理所有排隊的執行選擇器呼叫,而不是在每次迴圈迭代期間處理一個。

     下面列出了NSObject上定義的方法,可用於在其他執行緒上執行選擇器。因為這些方法是在NSObject上宣告的,所以我們可以在任何有權訪問NSObject物件的執行緒中使用它們,包括POSIX執行緒。這些方法實際上不會建立一個新的執行緒來執行選擇器。

在其他執行緒上執行選擇器

方法1.

performSelectorOnMainThread:withObject:waitUntilDone:

performSelectorOnMainThread:withObject:waitUntilDone:modes:

以上方法在該執行緒的下一個執行迴圈中執行應用程式主執行緒上的指定選擇器。這些方法使我們可以選擇阻止當前執行緒,直到執行選擇器。

方法2.

performSelector:onThread:withObject:waitUntilDone:

performSelector:onThread:withObject:waitUntilDone:modes:

以上方法在擁有NSThread物件的任何執行緒上執行指定的選擇器。這些方法可以選擇是否阻止當前執行緒,直到執行選擇器。

方法3.

performSelector:withObject:afterDelay:

performSelector:withObject:afterDelay:inModes:

以上方法在下一個執行迴圈週期和可選的延遲週期之後,在當前執行緒上執行指定的選擇器。由於它等待下一個RunLoop執行選擇器,所以這些方法會從當前正在執行的程式碼中提供一個微型的自動延遲。多個排隊選擇器按照他們排隊的順序依次執行。

方法4.

cancelPreviousPerformRequestsWithTarget:

cancelPreviousPerformRequestsWithTarget:selector:object:

以上方法允許我們使用performSelector:withObject:afterDelay:或performSelector:withObject:afterDelay:inModes:方法取消傳送到當前執行緒的訊息。

1.2.4:定時器源

     定時器源在未來的預設時間將事件同步傳遞給執行緒。定時器源是執行緒通知自己做某事的一種方式。例如,搜尋欄位可以使用定時器在使用者的連續擊鍵之間經過一段之間後啟動自動搜尋。使用此延遲時間使使用者有機會再開始搜尋之前儘可能多地輸入所需的搜尋字串。

     雖然它生成基於時間的通知,但計時器不是實時機制。與輸入源一樣,定時器與RunLoop的特定模式相關聯。如果一個定時器不處於當前由RunLoop監視的模式,那麼只有在定時器的一種受支援模式下執行RunLoop時才會觸發定時器。同樣,如果執行迴圈處於處理程式歷程的中間時觸發定時器,則定時器將等待下一次通過RunLoop來呼叫其處理程式例程。如果RunLoop根本沒有執行,則定時器不會啟動。

     我們可以將定時器配置為僅生成一次或重複生成事件。重複計時器會根據預定的執行時間自動重新安排時間,而不是實際的執行時間。例如,如果定時器計劃在特定的執行時間以及之後每隔5秒觸發一次,則即使實際觸發時間延遲,計劃的觸發時間也會始終以最初的5秒為間隔。如果開始時間延遲太多以至於未能到達一個或者多個預定的執行時間,則計時器在錯過的時間段內僅被執行一次。在錯過的執行時間後,定時器重新安排下一個預設的執行時間。

有關配置定時器源的更多資訊,請參閱3.2配置定時器源。有關參考資訊,請參閱NSTimer類參考CFRunLoopTimer參考

1.2.5:RunLoop觀察器

     與發生適當非同步事件或同步事件時觸發的源相比,RunLoop觀察器在RunLoop本身的執行過程中會在特定位置觸發。我們可以使用RunLoop觀察器來準備執行緒以及處理給定的事件,或者線上程進入休眠之前準備執行緒。我們可以將RunLoop觀察程式與RunLoop中的以下事件相關聯:

1.進入RunLoop。

2.RunLoop即將處理計時器。

3.RunLoop即將處理輸入源。

4.RunLoop即將進入睡眠狀態。

5.RunLoop喚醒,但在處理喚醒他的事件之前。

6.退出RunLoop。

     我們可以使用Core Foundation將RunLoop觀察器新增到應用程式。要建立RunLoop觀察器,可以建立CFRunLoopObserverRef不透明型別的例項。此型別會跟蹤自定義您的自定義回撥函式以及它感興趣的活動。

     與定時器類似,RunLoop觀察器可以使用一次或重複使用。一次觀察者在執行回撥函式後從RunLoop中刪除,而重複觀察者仍然在RunLoop中。我們可以指定觀察者在建立時是執行一次還是重複執行。

有關如何建立執行迴圈觀察程式的示例,請參考2.2配置執行迴圈。有關參考資訊,請參閱CFRunLoopObserver參考

1.2.6:事件的RunLoop執行次序

     每次執行RunLoop時,執行緒的RunLoop都會處理未決事件,併為任何附加的觀察者生成通知。它的執行順序非常具體,如下所示:

1.通知觀察者已經進入RunLoop。

2.通知觀察者計時器執行準備就緒。

3.通知觀察者,非基於埠的輸入源即將觸發。

4.啟動任何可以觸發的非基於埠的輸入源。

5.如果基於埠的輸入源已準備好並正在等待觸發,請立即處理該事件。跳轉到第9步。

6.通知觀察者該執行緒即將進入休眠。

7.讓執行緒進入休眠狀態,直到發生以下事件之一:

<1>一個基於埠的輸入源事件到達。

<2>計時器啟動。

<3>RunLoop超時。

<4>RunLoop被明確地喚醒。

8.通知觀察者執行緒剛剛喚醒。

9。處理未決事件

<1>如果使用者定義的定時器啟動,則處理定時器事件並重新啟動迴圈。跳轉到第2步。

<2>如果輸入源被觸發,則交付事件。

<3>如果RunLoop顯式喚醒但尚未超時,重新啟動RunLoop。跳轉到第2步。

10.通知觀察者RunLoop已退出。

     因為定時器和輸入源的觀察者通知是在這些事件實際發生前交付的,所以通知時間和實際事件時間之間可能存在差距。如果這些事件之間的時間很關鍵,則可以使用休眠和從休眠中醒來的通知來完成關聯實際事件之間的時間。

     由於定時器和其他週期性事件是在執行迴圈時交付的,因此繞過該RunLoop會中斷這些事件 的交付。無論何時通過輸入一個迴圈並重復地從應用程式請求事件來實現滑鼠跟蹤例程,都會發生此行為的典型示例。由於我們的程式碼直接抓取事件,而不是讓應用程式正常排程這些事件,因此在滑鼠跟蹤例程退出並將控制返回給應用程式之前,啟用的定時器將無法觸發。

     RunLoop可以使用RunLoop物件顯式喚醒。其他事件也可能導致RunLoop被喚醒。例如,新增另一個基於非埠的輸入源會喚醒RunLoop,以便於可以立即處理輸入源,而不是等待其他事件發生。

二:使用RunLoop物件

     RunLoop物件提供了將輸入源,定時器和執行迴圈觀察器新增到RunLoop並執行它的主要介面。每個執行緒都有一個與之關聯的RunLoop物件。在Cocoa中,這個物件是NSRunLoop類的一個例項。在底層的應用程式中,它是一個指向CFRunLoopRef型別的指標。

2.1.獲取RunLoop物件

可以使用如下方式來獲取當前執行緒的RunLoop:

<1>在Cocoa應用程式中,使用NSRunLoop的currentRunLoop類方法來檢索NSRunLoop物件。

<2>使用CFRunLoopGetCurrent函式。

     雖然它們不是免費的橋接型別(toll-free bridged types),但在需要時,我們可以從NSRunLoop 物件獲取CFRunLoopRef不透明型別。NSRunLoop類定義了一個getCFRunLoop方法,該方法返回可以傳遞給Core Foundation例程的CFRunLoopRef型別。由於兩個物件都引用同一個RunLoop,因此可以根據需要將呼叫混合到NSRunLoop物件和CFRunLoopRef不透明型別。

2.2.配置RunLoop

     在子執行緒上執行RunLoop之前,我們必須至少新增一個輸入源或計時器。如果RunLoop沒有任何監控的來源,當嘗試執行RunLoop時,它會立即退出。有關如何將源新增到RunLoop的示例,請參考第三節配置RunLoop源

     除了安裝原始碼之外,我們還可以安裝RunLoop觀察器並使用它們來檢測RunLoop的不同執行階段。要安裝RunLoop觀察器,需要建立一個CFRunLoopObserverRef 不透明型別,並使用CFRunLoopAddObserver函式將其新增到RunLoop中。RunLoop觀察者必須使用Core Foundation建立,即使對於Cocoa應用程式也是如此。

     圖(2-1-1)顯示了將RunLoop觀察器連線到其RunLoop的執行緒的主例程。該示例的目的是向您展示如何建立RunLoop觀察器,因此程式碼只需要設定一個RunLoop觀察器即可監視所有RunLoop活動。基本處理程式例程只是在處理計時器請求時記錄RunLoop活動。

[譯]iOS RunLoops
                                                    圖(2-1-1)新增關聯RunLoop的觀察器

     如果我們想配置一個一直執行的RunLoop,最好新增至少一個輸入源接收訊息。儘管我們可以在進入RunLoop是關聯一個定時器,一旦定時器開始,它通常會面臨失效,一旦定時器失效RunLoop便會退出。附加重複計時器可以使RunLoop長時間執行,但是會定時觸發計時器喚醒執行緒,這實際上是另一種輪詢方式。相反,輸入源會等待事件發生,讓執行緒一直處於休眠狀態,知道它(事件)發生。

2.3.啟動RunLoop

     啟動RunLoop僅適用於應用程式中的子執行緒。RunLoop必須至少有一個輸入源和計時器才能進行監視。如果沒有,則RunLoop立即退出。

有以下幾種開啟RunLoop的方式,包括:

<1>無條件的

<2>具有設定的時間限制

<3>在特定模式下

     無條件的進入RunLoop是最簡單的選擇,但他也是最不可取的。無條件的執行你的RunLoop會把你的執行緒放到一個永久迴圈中,這使你很少控制RunLoop本身。我們可以新增和刪除輸入源和定時器,但停止RunLoop的唯一方法是殺死它,同樣在自定義模式下,我們也無法執行RunLoop。

     不使用無條件地執行RunLoop,最好使用超時值執行RunLoop。當我們設定超時值時,RunLoop會一直執行,知道事件到達或分配的時間到期。如果事件到達,則將該事件分派給處理程式進行處理,然後RunLoop退出。我們使用程式碼重新啟動RunLoop來處理下一個事件。如果分配的時間到期,我們可以簡單地重新啟動RunLoop或使用時間來完成所需的任務管理。

     除了超時值之外,我們也可以使用特定模式執行RunLoop。模式和超時值不是互斥的,並且在啟動RunLoop時都可以使用。模式限制將事件傳遞到RunLoop的源的型別,並在RunLoop模式中有更詳細的描述。

     圖(2-1-2)顯示了一個執行緒的主要入口示例的框架版本。這個例子的關鍵部分顯示了RunLoop的基本結構。本質上,我們將輸入源和定時器新增到RunLoop中,然後重複呼叫其中一個示例來啟動RunLoop。每次RunLoop示例返回時,都會檢查是否有任何可能導致退出執行緒的情況。該示例使用Core FoundationRunLoop示例,以便它可以檢查返回結果並確定RunLoop退出的原因。如果使用Cocoa並且不需要檢查返回值,我們也可以使用NSRunLoop類的方法以類似的方式執行RunLoop。(有關呼叫NSRunLoop類的方法執行迴圈的示例,請參考圖(3-3-3)。)

[譯]iOS RunLoops
                                                     圖(2-1-2)執行一個RunLoop

     它可以遞迴執行一個RunLoop。換句話說,我們可以呼叫CFRunLoopRun,CFRunLoopRunInMode或任何NSRunLoop方法來從輸入源或定時器的處理程式中啟動RunLoop。當這樣做時,可以使用任何mode執行巢狀的RunLoop,包括外部巢狀使用的模式。

2.4.退出RunLoop

在處理事件之前,有兩種方式可以使RunLoop退出:

<1>以超時值配置RunLoop執行。

<2>告訴RunLoop停止。

     如果可以管理它,使用超時值肯定是首選。指定超時值可讓RunLoop完成所有正常處理,包括在退出之前將通知傳送到RunLoop觀察器。

     使用CFRunLoopStop函式顯式停止RunLoop會產生類似於超時的結果。RunLoop發出任何剩餘的RunLoop通知,然後退出。不同的是,我們可以在無條件開啟的RunLoop中使用此技術。

      儘管刪除RunLoop的輸入源和定時器也可能導致RunLoop退出,但這不是停止RunLoop的可靠方法。一些系統例程將輸入源新增到RunLoop以處理所需的事件。但是我們的程式碼可能沒有意識到這些輸入源,所以它將無法刪除他們,這將阻止RunLoop退出。

2.5.執行緒安全和RunLoop物件

     執行緒安全取決於我們使用哪個API來操作執行迴圈。Core Foundation中的函式通常是執行緒安全的,可以從任何執行緒中呼叫。但是,如果要執行更改RunLoop配置的操作,則儘可能從擁有RunLoop的執行緒執行此操作仍是一種好的做法。

     Cocoa 中的NSRunLoop類並不想其核心機處物件那樣天生就是執行緒安全的。如果使用NSRunLoop類來修改RunLoop,則應該只從擁有該RunLoop的同一個執行緒來完成。將輸入源或定時器源新增到屬於不同執行緒的RunLoop中可能會導致程式碼崩潰或以意外的方式執行。

三:配置RunLoop源

以下部分顯示瞭如何在Cocoa和Core Foundation中設定不同型別的輸入源示例。

3.1.定義自定義輸入源

建立自定義輸入源包括定義以下內容:

<1>希望輸入源處理的資訊。

<2>排程程式讓有興趣的客戶知道如何聯絡您的輸入源。

<3>處理程式例程,用於執行任何客戶端傳送的請求。

<4>取消例程以使輸入源無效。

     由於我們建立了一個自定義的輸入源來處理自定義的資訊,因此實際配置是設計靈活的。排程程式,處理程式和取消例程幾乎總是需要用於自定義輸入源的關鍵例程。然而,大部分輸入源的行為的其餘部分都發生在這些處理程式之外。例如,我們需要定義將資料傳遞到輸入源並將輸入源的存在傳遞給其他執行緒的機制。

     下圖(3-1)顯示了自定義輸入源的示例配置。在本例中,應用程式的主執行緒保持對輸入源,該輸入源的自定義命令緩衝區以及安裝輸入源的RunLoop的引用。當主執行緒有一個任務想要切換到工作執行緒時,它將命令傳送到命令緩衝區以及工作執行緒啟動任務所需的任何資訊。(因為工作執行緒的主執行緒和輸入源都可以訪問命令緩衝區,所以訪問必須同步。)一旦命令釋出,主執行緒就會發訊號通知輸入源並喚醒工作執行緒的RunLoop。在收到喚醒命令後,RunLoop會呼叫輸入源的處理程式,該輸入源處理命令緩衝區中的命令。

[譯]iOS RunLoops
                                  圖(3-1)操作自定義的輸入源

下面各節將解釋上圖中自定義輸入源的實現,並顯示需要實現的關鍵程式碼。

3.1.1.定義輸入源

     定義自定義輸入源需要使用Core Foundation例程來配置RunLoop源並將其附加到RunLoop。雖然基本的處理程式是基於C的函式,但這並不妨礙我們為這些函式編寫包裝,並使用Objective-C或C++來實現程式碼的主題。

     圖3-1中介紹的輸入源使用Objective-C物件來管理命令緩衝區並與RunLoop進行協調。圖(3-1-1)顯示了這個物件的定義。RunLoopSpurce物件管理命令緩衝區並使用該緩衝區接收來自其他執行緒的訊息。此圖還顯示RunLoopContext物件的自定義,該物件實際上只是一個容器,用於將RunLoopSpurce物件和RunLoop引用傳遞給應用程式的主執行緒。

[譯]iOS RunLoops
                                                  圖(3-1-1)定義自定義的輸入源

     雖然Objective-C程式碼管理輸入源的自定義資料,但將輸入源附加到RunLoop中需要使用基於C的回撥函式。當我們將RunLoop源實際連線到RunLoop時,將呼叫其中的第一個函式如圖(3-1-2)所示。由於此輸入源只有一個客戶端(主執行緒),因此它使用排程程式函式傳送訊息以在該執行緒上嚮應用程式委託註冊自己。當委託人想要與輸入源通訊時,它使用RunLoopContext物件中的資訊來執行此操作。

[譯]iOS RunLoops
                                                 圖(3-1-2)執行RunLoop源

     最重要的回撥例程之一是用於在輸入源傳送訊號時處理自定義資料的回撥例程。圖(3-1-3)顯示了RunLoopSource物件關聯的執行回撥示例。該函式只是將請求執行的請求轉發給SourceFired方法,然後該方法處理命令緩衝區中存在的任何命令。

[譯]iOS RunLoops
                                               圖(3-1-3)在輸入源中執行任務

     如果使用CFRunLoopSourceInvalidate函式將輸入源從其RunLoop中移除,系統將呼叫輸入源取得取消例程。我們可以使用此例程來通知客戶端輸入源不再有效,並且應該刪除對它的引用。圖(3-1-4)顯示了用RunLoopSpurce物件註冊的取消回撥例程。該函式將另一個RunLoopContext物件傳送給應用程式委託,但是這次要求委託移除對RunLoop源的引用。

[譯]iOS RunLoops
                                                     圖(3-1-4)使輸入源無效

注意:應用程式委託的registerSource:和removeSource:方法的程式碼顯示在3.1.3節與輸入源的客戶端協調中

3.1.2在RunLoop中安裝輸入源

     圖3-2-1顯示了RunLoopSource類的init和addToCurrentRunLoop方法。init方法建立實際連線到RunLoop的CFRunLoopSourceRef不透明型別。它將RunLoopSource物件本身作為上下文資訊傳遞,以便回撥例程具有指向該物件的指標。在工作執行緒呼叫addToCurrentRunLoop方法之前,不會發生輸入源的安裝,此時將呼叫RunLoopSourceScheduleRoutine回撥函式。一旦輸入源被新增到RunLoop中,執行緒就可以執行它的RunLoop來等待它。

[譯]iOS RunLoops
                                                       圖(3-1-5)安裝RunLoop源

3.1.3與輸入源的客戶端協調

     為了使輸入源有用,我們需要操作它並從另一個執行緒發出訊號。輸入源的全部要點是將其關聯的執行緒休眠直到有事情要做。這個事實需要應用程式中的其他執行緒知道輸入源並且有一個與之通訊的方法。

     將輸入源首次安裝在RunLoop中時,向客戶端通知輸入源的一種方法是傳送註冊請求。我們可以根據輸入源註冊儘可能多的客戶端,或者可以將其註冊到某個中央機構,然後將輸入源釋出給感興趣的客戶端。圖(3-1-6)顯示了由應用程式委託定義並在呼叫RunLoopSource物件的排程程式函式時呼叫註冊方法。該方法接收由RunLoopSource物件提供的RunLoopContext物件,並將其新增到其源列表中。此列表還顯示用於在從RunLoop中刪除輸入源時登出輸入源的例程。

[譯]iOS RunLoops
                                  圖(3-1-6)使用應用程式代理註冊和刪除輸入源

3.1.4傳送輸入源訊號

     將資料傳遞到輸入源後,客戶端必須發出訊號並喚醒其RunLoop。訊號源讓RunLoop知道源已準備好處理。而且因為執行緒在訊號發生時可能會休眠,因此應該始終明確的喚醒RunLoop。如果不這樣做,可能會導致處理輸入源的延遲。

     圖(3-2-3)顯示了RunLoopSource物件的fireCommandsOnRunLoop方法。當客戶端準備好處理新增緩衝區的命令時,客戶端會呼叫此方法。

[譯]iOS RunLoops
                                                       圖(3-2-3)喚醒RunLoop

注意:我們不應該嘗試通過傳送自定義輸入源來處理SIGHUP或其他型別的過程級訊號。Core Foundation喚醒RunLoop的方法不是訊號安全的,不應該在應用程式的訊號處理程式中使用。

3.2.配置定時器源

     要建立一個計時器源,首先建立一個計時器物件並將其安排在RunLoop中。在Cocoa中,使用NSTimer類來建立新的計時器物件,在Core Foundation中使用CFRunLoopTimerRef不透明型別。在內部,NSTimer類只是Core Foundation的擴充套件,它提供了一些便利功能,如使用相同的方法建立和安排定時器的能力。

在Cocoa中,可以使用以下任一類方法建立和安排計時器:

<1>scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:

<2>scheduledTimerWithTimeInterval:invocation:repeats:

     這些方法建立計時器並將其新增到當前執行緒的預設模式下的RunLoop(NSDefaultRunLoopMode)。如果想建立一個NSTimer物件,然後使用NSRunLoop的addTimer:forMode:方法將它新增到RunLoop中,也可以手動安排定時器。這兩種技術基本上都是相同的,但是對定時器配置級別的控制是不同的。如建立計時器並將其手動新增到RunLoop中則可以使用除預設模式之外的模式執行此操作。圖(3-2-1)顯示瞭如何使用這兩種技術建立定時器。第一個定時器的初始延遲時間為1秒,但之後每0.1秒定時觸發一次。第二個定時器在初始0.2秒延遲後開始執行,然後每0.2秒執行一次。

[譯]iOS RunLoops
                                            圖(3-2-1)使用NSTimer建立安排計時器

     圖(3-2-2)顯示了Core Foundation配置計時器所需的程式碼。雖然此示例沒有在上下文中傳遞任何使用者定義的資訊,但可以使用此結構來傳遞計時器所需的自定義資料。

[譯]iOS RunLoops
                               圖(3-2-2)使用CoreFoundation建立和安排計時器

3.3.配置基於埠的輸入源

     Cocoa和Core Foundation都提供了用於執行緒間或程式間通訊的基於埠的物件。下面介紹如何使用幾種不同型別的埠設定埠通訊。

3.3.1配置一個NSMachPort物件

     要與NSMachPort物件建立本地連線,需要建立埠物件並將其新增到主執行緒的RunLoop中。啟動輔助執行緒時,將相同的物件傳遞給執行緒的入口函式。子執行緒可以使用相同的物件將訊息傳送回主執行緒。

<1>主執行緒的實現程式碼

     圖(3-3-1)顯示了啟動輔助工作執行緒的主執行緒程式碼。因為Cocoa框架執行許多配置埠和RunLoop的干預步驟,所以lanuchThread方法明顯短於其Core Foundation等價物;然而兩者的行為幾乎完全相同。一個區別是該方法不是將本地埠的名稱傳送給工作執行緒,而是直接傳送給NSPort物件。

[譯]iOS RunLoops
                                                      圖(3-3-1)主執行緒啟動方法

     為了線上程之間建立一個雙向通訊通道,我們可能希望工作執行緒在check-in訊息中傳送本地埠到主執行緒。收到check-in訊息後,主執行緒會知道啟動第二執行緒時一切順利,並且還提供了一種將更多訊息傳送到該執行緒的方法。

     圖(3-3-2)顯示了主執行緒的handlePortMessage:方法。當資料到達執行緒自己的本地埠時呼叫此方法。當check-in訊息到達時,該方法直接從埠訊息中檢索輔助執行緒的埠並將其儲存以供以後使用。

[譯]iOS RunLoops
                                                    圖(3-3-2)處理Mach埠訊息

注意:如果您建立的是iOS專案此程式碼會報錯,因為NSPortMessage目前只支援macOS 10.0+之後的系統。

<2>輔助工作執行緒的實現程式碼

     對於輔助工作執行緒,必須使用指定的埠配置執行緒並將資訊傳回主執行緒。

     圖(3-3-3)顯示了設定工作執行緒的程式碼。為執行緒建立一個自動釋放池,該方法建立一個工作物件來驅動執行緒執行。工作物件的sendCheckinMessage:方法(圖3-3-4)為工作執行緒建立一個本地埠,並將一個check-in訊息發回主執行緒。

[譯]iOS RunLoops
                                             圖(3-3-3)使用Mach埠啟動工作執行緒

     使用NSMachPort時,本地和遠端執行緒可以使用相同的埠物件進行執行緒之間的單向通訊。換句話說,由一個執行緒建立的本地埠物件成為另一個執行緒的遠端埠物件。

     圖(3-3-4)顯示了輔助執行緒的check-in示例。此方法為將來的通訊設定了自己的本地埠,然後將check-in訊息傳送回主執行緒。該方法使用LanuchThreadWithPort:方法中收到的埠物件作為訊息的目標。

[譯]iOS RunLoops
                                  圖(3-3-4)使用Mach埠傳送check-in訊息

3.3.2配置一個NSMessagePort物件

     要與NSMessagePort物件建立本地連線,我們不能簡單地線上程之間傳遞埠物件。遠端訊息埠必須按名稱獲取。在Cocoa中實現這一點需要註冊一個特定名稱的本地埠,然後將該名稱傳遞給遠端執行緒,以便它可以獲取適當的埠物件進行通訊。圖(3-3-5)顯示了使用訊息埠的情況下埠的建立和註冊過程。

[譯]iOS RunLoops
                                                        圖(3-3-5)註冊訊息埠

3.3.3在Core Foundation中配置基於埠的輸入源

     這裡介紹如何使用Core Foundation在應用程式的主執行緒和工作執行緒之間建立雙向通訊通道。

     圖(3-3-6)顯示了應用程式主執行緒呼叫的啟動工作執行緒的程式碼。程式碼首先設定一個CFMessagePortRef不透明型別來偵聽來自工作執行緒的訊息。工作執行緒需要埠的名稱來建立連線,以便將字串值傳遞給工作執行緒入口點函式。埠名稱在當前使用者上下文中通常應該是唯一的;否則,可能會遇到衝突。

[譯]iOS RunLoops
                                                                 圖(3-3-6)
[譯]iOS RunLoops
                          圖(3-3-6)為新執行緒附加一個Core Foundation訊息埠


     在安裝了埠並啟動了執行緒的情況下,主執行緒可以在等待執行緒check-in時繼續執行常規執行。當check-in訊息到達時,它將被分派到主執行緒的MainThreadResponseHandler函式中,如圖(3-3-7)。此函式提取工作執行緒的埠名稱併為未來的通訊建立管道。

[譯]iOS RunLoops
                                                                  圖(3-3-7)
[譯]iOS RunLoops
                                                   圖(3-3-7)接收check-in訊息

     在配置主執行緒後,剩餘的唯一東西是新建立的工作執行緒建立自己的埠並進行check-in。圖(3-3-8)顯示了工作執行緒的入口點函式。該函式提取主執行緒的埠名稱並使用它來建立遠端連線回主執行緒。然後該函式為自己建立一個本地埠,在該執行緒的RunLoop中安裝埠,並向包含本地埠名稱的主執行緒傳送一個check-in訊息。

[譯]iOS RunLoops
                                                               圖(3-3-8)
[譯]iOS RunLoops
                                                                     圖(3-3-8)
[譯]iOS RunLoops
                                                     圖(3-3-8)設定執行緒結構

     一旦它進入RunLoop,傳送到執行緒埠的所有未來事件都將有ProcessClientRequest函式處理。該函式的實現取決於執行緒所執行的工作型別,在此不顯示。


相關文章