翻譯來源: ?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退出。
????????除了處理輸入源(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活動。
????????如果我們想配置一個一直運行的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)。)
????????它可以遞歸運行一個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會調用輸入源的處理程序,該輸入源處理命令緩沖區中的命令。
下面各節將解釋上圖中自定義輸入源的實現,并顯示需要實現的關鍵代碼。
3.1.1.定義輸入源
????????定義自定義輸入源需要使用Core Foundation例程來配置RunLoop源并將其附加到RunLoop。雖然基本的處理程序是基于C的函數,但這并不妨礙我們為這些函數編寫包裝,并使用Objective-C或C++來實現代碼的主題。
????????圖3-1中介紹的輸入源使用Objective-C對象來管理命令緩沖區并與RunLoop進行協調。圖(3-1-1)顯示了這個對象的定義。RunLoopSpurce對象管理命令緩沖區并使用該緩沖區接收來自其他線程的消息。此圖還顯示RunLoopContext對象的自定義,該對象實際上只是一個容器,用于將RunLoopSpurce對象和RunLoop引用傳遞給應用程序的主線程。
????????雖然Objective-C代碼管理輸入源的自定義數據,但將輸入源附加到RunLoop中需要使用基于C的回調函數。當我們將RunLoop源實際連接到RunLoop時,將調用其中的第一個函數如圖(3-1-2)所示。由于此輸入源只有一個客戶端(主線程),因此它使用調度程序函數發送消息以在該線程上向應用程序委托注冊自己。當委托人想要與輸入源通信時,它使用RunLoopContext對象中的信息來執行此操作。
????????最重要的回調例程之一是用于在輸入源發送信號時處理自定義數據的回調例程。圖(3-1-3)顯示了RunLoopSource對象關聯的執行回調示例。該函數只是將請求執行的請求轉發給SourceFired方法,然后該方法處理命令緩沖區中存在的任何命令。
????????如果使用CFRunLoopSourceInvalidate函數將輸入源從其RunLoop中移除,系統將調用輸入源取得取消例程。我們可以使用此例程來通知客戶端輸入源不再有效,并且應該刪除對它的引用。圖(3-1-4)顯示了用RunLoopSpurce對象注冊的取消回調例程。該函數將另一個RunLoopContext對象發送給應用程序委托,但是這次要求委托移除對RunLoop源的引用。
注意:應用程序委托的registerSource:和removeSource:方法的代碼顯示在3.1.3節與輸入源的客戶端協調中。
3.1.2在RunLoop中安裝輸入源
????????圖3-2-1顯示了RunLoopSource類的init和addToCurrentRunLoop方法。init方法創建實際連接到RunLoop的CFRunLoopSourceRef不透明類型。它將RunLoopSource對象本身作為上下文信息傳遞,以便回調例程具有指向該對象的指針。在工作線程調用addToCurrentRunLoop方法之前,不會發生輸入源的安裝,此時將調用RunLoopSourceScheduleRoutine回調函數。一旦輸入源被添加到RunLoop中,線程就可以運行它的RunLoop來等待它。
3.1.3與輸入源的客戶端協調
????????為了使輸入源有用,我們需要操作它并從另一個線程發出信號。輸入源的全部要點是將其關聯的線程休眠直到有事情要做。這個事實需要應用程序中的其他線程知道輸入源并且有一個與之通信的方法。
????????將輸入源首次安裝在RunLoop中時,向客戶端通知輸入源的一種方法是發送注冊請求。我們可以根據輸入源注冊盡可能多的客戶端,或者可以將其注冊到某個中央機構,然后將輸入源發布給感興趣的客戶端。圖(3-1-6)顯示了由應用程序委托定義并在調用RunLoopSource對象的調度程序函數時調用注冊方法。該方法接收由RunLoopSource對象提供的RunLoopContext對象,并將其添加到其源列表中。此列表還顯示用于在從RunLoop中刪除輸入源時注銷輸入源的例程。
3.1.4發送輸入源信號
????????將數據傳遞到輸入源后,客戶端必須發出信號并喚醒其RunLoop。信號源讓RunLoop知道源已準備好處理。而且因為線程在信號發生時可能會休眠,因此應該始終明確的喚醒RunLoop。如果不這樣做,可能會導致處理輸入源的延遲。
????????圖(3-2-3)顯示了RunLoopSource對象的fireCommandsOnRunLoop方法。當客戶端準備好處理添加緩沖區的命令時,客戶端會調用此方法。
注意:我們不應該嘗試通過發送自定義輸入源來處理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秒執行一次。
????????圖(3-2-2)顯示了Core Foundation配置計時器所需的代碼。雖然此示例沒有在上下文中傳遞任何用戶定義的信息,但可以使用此結構來傳遞計時器所需的自定義數據。
3.3.配置基于端口的輸入源
????????Cocoa和Core Foundation都提供了用于線程間或進程間通信的基于端口的對象。下面介紹如何使用幾種不同類型的端口設置端口通信。
3.3.1配置一個NSMachPort對象
????????要與NSMachPort對象建立本地連接,需要創建端口對象并將其添加到主線程的RunLoop中。啟動輔助線程時,將相同的對象傳遞給線程的入口函數。子線程可以使用相同的對象將消息發送回主線程。
<1>主線程的實現代碼
????????圖(3-3-1)顯示了啟動輔助工作線程的主線程代碼。因為Cocoa框架執行許多配置端口和RunLoop的干預步驟,所以lanuchThread方法明顯短于其Core Foundation等價物;然而兩者的行為幾乎完全相同。一個區別是該方法不是將本地端口的名稱發送給工作線程,而是直接發送給NSPort對象。
????????為了在線程之間建立一個雙向通信通道,我們可能希望工作線程在check-in消息中發送本地端口到主線程。收到check-in消息后,主線程會知道啟動第二線程時一切順利,并且還提供了一種將更多消息發送到該線程的方法。
????????圖(3-3-2)顯示了主線程的handlePortMessage:方法。當數據到達線程自己的本地端口時調用此方法。當check-in消息到達時,該方法直接從端口消息中檢索輔助線程的端口并將其保存以供以后使用。
注意:如果您創建的是iOS項目此代碼會報錯,因為NSPortMessage目前只支持macOS 10.0+之后的系統。
<2>輔助工作線程的實現代碼
????????對于輔助工作線程,必須使用指定的端口配置線程并將信息傳回主線程。
????????圖(3-3-3)顯示了設置工作線程的代碼。為線程創建一個自動釋放池,該方法創建一個工作對象來驅動線程執行。工作對象的sendCheckinMessage:方法(圖3-3-4)為工作線程創建一個本地端口,并將一個check-in消息發回主線程。
????????使用NSMachPort時,本地和遠程線程可以使用相同的端口對象進行線程之間的單向通信。換句話說,由一個線程創建的本地端口對象成為另一個線程的遠程端口對象。
????????圖(3-3-4)顯示了輔助線程的check-in示例。此方法為將來的通信設置了自己的本地端口,然后將check-in消息發送回主線程。該方法使用LanuchThreadWithPort:方法中收到的端口對象作為消息的目標。
3.3.2配置一個NSMessagePort對象
????????要與NSMessagePort對象建立本地連接,我們不能簡單地在線程之間傳遞端口對象。遠程消息端口必須按名稱獲取。在Cocoa中實現這一點需要注冊一個特定名稱的本地端口,然后將該名稱傳遞給遠程線程,以便它可以獲取適當的端口對象進行通信。圖(3-3-5)顯示了使用消息端口的情況下端口的創建和注冊過程。
3.3.3在Core Foundation中配置基于端口的輸入源
????????這里介紹如何使用Core Foundation在應用程序的主線程和工作線程之間建立雙向通信通道。
????????圖(3-3-6)顯示了應用程序主線程調用的啟動工作線程的代碼。代碼首先設置一個CFMessagePortRef不透明類型來偵聽來自工作線程的消息。工作線程需要端口的名稱來建立連接,以便將字符串值傳遞給工作線程入口點函數。端口名稱在當前用戶上下文中通常應該是唯一的;否則,可能會遇到沖突。
? ? ? ? 在安裝了端口并啟動了線程的情況下,主線程可以在等待線程check-in時繼續執行常規執行。當check-in消息到達時,它將被分派到主線程的MainThreadResponseHandler函數中,如圖(3-3-7)。此函數提取工作線程的端口名稱并為未來的通信創建管道。
? ? ? ? 在配置主線程后,剩余的唯一東西是新創建的工作線程創建自己的端口并進行check-in。圖(3-3-8)顯示了工作線程的入口點函數。該函數提取主線程的端口名稱并使用它來創建遠程連接回主線程。然后該函數為自己創建一個本地端口,在該線程的RunLoop中安裝端口,并向包含本地端口名稱的主線程發送一個check-in消息。
? ? ? ? 一旦它進入RunLoop,發送到線程端口的所有未來事件都將有ProcessClientRequest函數處理。該函數的實現取決于線程所執行的工作類型,在此不顯示。