翻譯來源: RunLoops html
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.app
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觀察器。oop
下面的部分提供了關於RunLoop的組件和其運行模式的更多信息。它們還描述了在處理事件期間在不一樣時間生成的通知。ui
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函數將自定義模式添加到集合。
輸入源以異步的方式向您的線程傳遞事件。事件的來源取決於輸入源的類型,它一般是兩類中的一類。基於端口的輸入源監視你的應用程序的Mach端口。自定義輸入源監視自定義事件源。就RunLoop而言,輸入源是基於端口的仍是自定義的應該沒有關係。系統一般實現兩種類型的輸入源,咱們能夠按照原樣使用它們。兩個來源之間的惟一區別是他們如何發出信號。基於端口的源由內核自動發送信號,自定義源必須從另外一個線程手動發送信號。
建立輸入源時,能夠將其分配給RunLoop的一個或多個模式。模式會影響在特定的時刻監視哪些輸入源。大多數狀況下,在默認模式下運行RunLoop,但也能夠指定自定義模式。若是輸入源不處於當前監控的模式,則會生成其生成的全部事件,直到RunLoop以正確的模式運行。
下面是各個輸入源的介紹。
Cocoa和Core Foundation爲使用與端口相關的對象和函數建立基於端口的輸入源提供了內置的支持。例如,在Cocoa中,咱們不須要直接建立輸入源。而是隻須要建立一個端口對象並使用NSPort的方法將該端口添加到RunLoop。port對象爲咱們處理所須要輸入源的建立和配置。
在Core Foundation中,咱們必須手動建立端口及其RunLoop源。在這兩種狀況下,都使用與端口不透明類型(CFMachPortRef,CFMessagePortRef或CFSocketRef)相關的函數來建立適當的對象。
有關如何設置和配置自定義的端口源的示例,請參閱3.3配置基於端口的輸入源。
要建立自定義的輸入源,咱們必須使用與Core Foundation中的 CFRunLoopSourceRef 不透明類型關聯的函數。咱們可使用多個回調函數來配置自定義的輸入源。Core Foundation在不一樣的點調用這些函數來配置源代碼,處理全部的傳入事件,並在源代碼從RunLoop中移除時刪除源代碼。
除了在事件到達時定義自定義源的行爲以外,還必須定義事件傳遞機制。這部分源代碼在單獨的線程上運行,負責爲輸入源提供數據,並在數據準備好處理時用信號通知它。事件傳遞機制取決於咱們,但沒必要過於複雜。
有關如何建立自定義輸入源的示例,請參閱下文定義自定義的輸入源。有關自定義輸入源的參考信息,請參閱CFRunLoopSource參考。
除了基於端口的源代碼以外,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:方法取消發送到當前線程的消息。
定時器源在將來的預設時間將事件同步傳遞給線程。定時器源是線程通知本身作某事的一種方式。例如,搜索字段可使用定時器在用戶的連續擊鍵之間通過一段之間後啓動自動搜索。使用此延遲時間使用戶有機會再開始搜索以前儘量多地輸入所需的搜索字符串。
雖然它生成基於時間的通知,但計時器不是實時機制。與輸入源同樣,定時器與RunLoop的特定模式相關聯。若是一個定時器不處於當前由RunLoop監視的模式,那麼只有在定時器的一種受支持模式下運行RunLoop時纔會觸發定時器。一樣,若是運行循環處於處理程序歷程的中間時觸發定時器,則定時器將等待下一次經過RunLoop來調用其處理程序例程。若是RunLoop根本沒有運行,則定時器不會啓動。
咱們能夠將定時器配置爲僅生成一次或重複生成事件。重複計時器會根據預約的執行時間自動從新安排時間,而不是實際的執行時間。例如,若是定時器計劃在特定的執行時間以及以後每隔5秒觸發一次,則即便實際觸發時間延遲,計劃的觸發時間也會始終以最初的5秒爲間隔。若是開始時間延遲太多以致於未能到達一個或者多個預約的執行時間,則計時器在錯過的時間段內僅被執行一次。在錯過的執行時間後,定時器從新安排下一個預設的執行時間。
有關配置定時器源的更多信息,請參閱3.2配置定時器源。有關參考信息,請參閱NSTimer類參考或CFRunLoopTimer參考。
與發生適當異步事件或同步事件時觸發的源相比,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參考
每次運行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對象。在Cocoa中,這個對象是NSRunLoop類的一個實例。在底層的應用程序中,它是一個指向CFRunLoopRef類型的指針。
可使用以下方式來獲取當前線程的RunLoop:
<1>在Cocoa應用程序中,使用NSRunLoop的currentRunLoop類方法來檢索NSRunLoop對象。
<2>使用CFRunLoopGetCurrent函數。
雖然它們不是免費的橋接類型(toll-free bridged types),但在須要時,咱們能夠從NSRunLoop 對象獲取CFRunLoopRef不透明類型。NSRunLoop類定義了一個getCFRunLoop方法,該方法返回能夠傳遞給Core Foundation例程的CFRunLoopRef類型。因爲兩個對象都引用同一個RunLoop,所以能夠根據須要將調用混合到NSRunLoop對象和CFRunLoopRef不透明類型。
在子線程上運行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長時間運行,可是會定時觸發計時器喚醒線程,這其實是另外一種輪詢方式。相反,輸入源會等待事件發生,讓線程一直處於休眠狀態,知道它(事件)發生。
啓動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,包括外部嵌套使用的模式。
在處理事件以前,有兩種方式可使RunLoop退出:
<1>以超時值配置RunLoop運行。
<2>告訴RunLoop中止。
若是能夠管理它,使用超時值確定是首選。指定超時值可以讓RunLoop完成全部正常處理,包括在退出以前將通知發送到RunLoop觀察器。
使用CFRunLoopStop函數顯式中止RunLoop會產生相似於超時的結果。RunLoop發出任何剩餘的RunLoop通知,而後退出。不一樣的是,咱們能夠在無條件開啓的RunLoop中使用此技術。
儘管刪除RunLoop的輸入源和定時器也可能致使RunLoop退出,但這不是中止RunLoop的可靠方法。一些系統例程將輸入源添加到RunLoop以處理所需的事件。可是咱們的代碼可能沒有意識到這些輸入源,因此它將沒法刪除他們,這將阻止RunLoop退出。
線程安全取決於咱們使用哪一個API來操做運行循環。Core Foundation中的函數一般是線程安全的,能夠從任何線程中調用。可是,若是要執行更改RunLoop配置的操做,則儘量從擁有RunLoop的線程執行此操做還是一種好的作法。
Cocoa 中的NSRunLoop類並不想其核心機處對象那樣天生就是線程安全的。若是使用NSRunLoop類來修改RunLoop,則應該只從擁有該RunLoop的同一個線程來完成。將輸入源或定時器源添加到屬於不一樣線程的RunLoop中可能會致使代碼崩潰或以意外的方式運行。
如下部分顯示瞭如何在Cocoa和Core Foundation中設置不一樣類型的輸入源示例。
建立自定義輸入源包括定義如下內容:
<1>但願輸入源處理的信息。
<2>調度程序讓有興趣的客戶知道如何聯繫您的輸入源。
<3>處理程序例程,用於執行任何客戶端發送的請求。
<4>取消例程以使輸入源無效。
因爲咱們建立了一個自定義的輸入源來處理自定義的信息,所以實際配置是設計靈活的。調度程序,處理程序和取消例程幾乎老是須要用於自定義輸入源的關鍵例程。然而,大部分輸入源的行爲的其他部分都發生在這些處理程序以外。例如,咱們須要定義將數據傳遞到輸入源並將輸入源的存在傳遞給其餘線程的機制。
下圖(3-1)顯示了自定義輸入源的示例配置。在本例中,應用程序的主線程保持對輸入源,該輸入源的自定義命令緩衝區以及安裝輸入源的RunLoop的引用。當主線程有一個任務想要切換到工做線程時,它將命令發送到命令緩衝區以及工做線程啓動任務所需的任何信息。(由於工做線程的主線程和輸入源均可以訪問命令緩衝區,因此訪問必須同步。)一旦命令發佈,主線程就會發信號通知輸入源並喚醒工做線程的RunLoop。在收到喚醒命令後,RunLoop會調用輸入源的處理程序,該輸入源處理命令緩衝區中的命令。
下面各節將解釋上圖中自定義輸入源的實現,並顯示須要實現的關鍵代碼。
定義自定義輸入源須要使用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-2-1顯示了RunLoopSource類的init和addToCurrentRunLoop方法。init方法建立實際鏈接到RunLoop的CFRunLoopSourceRef不透明類型。它將RunLoopSource對象自己做爲上下文信息傳遞,以便回調例程具備指向該對象的指針。在工做線程調用addToCurrentRunLoop方法以前,不會發生輸入源的安裝,此時將調用RunLoopSourceScheduleRoutine回調函數。一旦輸入源被添加到RunLoop中,線程就能夠運行它的RunLoop來等待它。
爲了使輸入源有用,咱們須要操做它並從另外一個線程發出信號。輸入源的所有要點是將其關聯的線程休眠直到有事情要作。這個事實須要應用程序中的其餘線程知道輸入源而且有一個與之通訊的方法。
將輸入源首次安裝在RunLoop中時,向客戶端通知輸入源的一種方法是發送註冊請求。咱們能夠根據輸入源註冊儘量多的客戶端,或者能夠將其註冊到某個中央機構,而後將輸入源發佈給感興趣的客戶端。圖(3-1-6)顯示了由應用程序委託定義並在調用RunLoopSource對象的調度程序函數時調用註冊方法。該方法接收由RunLoopSource對象提供的RunLoopContext對象,並將其添加到其源列表中。此列表還顯示用於在從RunLoop中刪除輸入源時註銷輸入源的例程。
將數據傳遞到輸入源後,客戶端必須發出信號並喚醒其RunLoop。信號源讓RunLoop知道源已準備好處理。並且由於線程在信號發生時可能會休眠,所以應該始終明確的喚醒RunLoop。若是不這樣作,可能會致使處理輸入源的延遲。
圖(3-2-3)顯示了RunLoopSource對象的fireCommandsOnRunLoop方法。當客戶端準備好處理添加緩衝區的命令時,客戶端會調用此方法。
注意:咱們不該該嘗試經過發送自定義輸入源來處理SIGHUP或其餘類型的過程級信號。Core Foundation喚醒RunLoop的方法不是信號安全的,不該該在應用程序的信號處理程序中使用。
要建立一個計時器源,首先建立一個計時器對象並將其安排在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配置計時器所需的代碼。雖然此示例沒有在上下文中傳遞任何用戶定義的信息,但可使用此結構來傳遞計時器所需的自定義數據。
Cocoa和Core Foundation都提供了用於線程間或進程間通訊的基於端口的對象。下面介紹如何使用幾種不一樣類型的端口設置端口通訊。
要與NSMachPort對象創建本地鏈接,須要建立端口對象並將其添加到主線程的RunLoop中。啓動輔助線程時,將相同的對象傳遞給線程的入口函數。子線程可使用相同的對象將消息發送回主線程。
圖(3-3-1)顯示了啓動輔助工做線程的主線程代碼。由於Cocoa框架執行許多配置端口和RunLoop的干預步驟,因此lanuchThread方法明顯短於其Core Foundation等價物;然而二者的行爲幾乎徹底相同。一個區別是該方法不是將本地端口的名稱發送給工做線程,而是直接發送給NSPort對象。
爲了在線程之間創建一個雙向通訊通道,咱們可能但願工做線程在check-in消息中發送本地端口到主線程。收到check-in消息後,主線程會知道啓動第二線程時一切順利,而且還提供了一種將更多消息發送到該線程的方法。
圖(3-3-2)顯示了主線程的handlePortMessage:方法。當數據到達線程本身的本地端口時調用此方法。當check-in消息到達時,該方法直接從端口消息中檢索輔助線程的端口並將其保存以供之後使用。
注意:若是您建立的是iOS項目此代碼會報錯,由於NSPortMessage目前只支持macOS 10.0+以後的系統。
對於輔助工做線程,必須使用指定的端口配置線程並將信息傳回主線程。
圖(3-3-3)顯示了設置工做線程的代碼。爲線程建立一個自動釋放池,該方法建立一個工做對象來驅動線程執行。工做對象的sendCheckinMessage:方法(圖3-3-4)爲工做線程建立一個本地端口,並將一個check-in消息發回主線程。
使用NSMachPort時,本地和遠程線程可使用相同的端口對象進行線程之間的單向通訊。換句話說,由一個線程建立的本地端口對象成爲另外一個線程的遠程端口對象。
圖(3-3-4)顯示了輔助線程的check-in示例。此方法爲未來的通訊設置了本身的本地端口,而後將check-in消息發送回主線程。該方法使用LanuchThreadWithPort:方法中收到的端口對象做爲消息的目標。
要與NSMessagePort對象創建本地鏈接,咱們不能簡單地在線程之間傳遞端口對象。遠程消息端口必須按名稱獲取。在Cocoa中實現這一點須要註冊一個特定名稱的本地端口,而後將該名稱傳遞給遠程線程,以便它能夠獲取適當的端口對象進行通訊。圖(3-3-5)顯示了使用消息端口的狀況下端口的建立和註冊過程。
這裏介紹如何使用Core Foundation在應用程序的主線程和工做線程之間創建雙向通訊通道。
圖(3-3-6)顯示了應用程序主線程調用的啓動工做線程的代碼。代碼首先設置一個CFMessagePortRef不透明類型來偵聽來自工做線程的消息。工做線程須要端口的名稱來創建鏈接,以便將字符串值傳遞給工做線程入口點函數。端口名稱在當前用戶上下文中一般應該是惟一的;不然,可能會遇到衝突。
在安裝了端口並啓動了線程的狀況下,主線程能夠在等待線程check-in時繼續執行常規執行。當check-in消息到達時,它將被分派到主線程的MainThreadResponseHandler函數中,如圖(3-3-7)。此函數提取工做線程的端口名稱併爲將來的通訊建立管道。
在配置主線程後,剩餘的惟一東西是新建立的工做線程建立本身的端口並進行check-in。圖(3-3-8)顯示了工做線程的入口點函數。該函數提取主線程的端口名稱並使用它來建立遠程鏈接回主線程。而後該函數爲本身建立一個本地端口,在該線程的RunLoop中安裝端口,並向包含本地端口名稱的主線程發送一個check-in消息。
一旦它進入RunLoop,發送到線程端口的全部將來事件都將有ProcessClientRequest函數處理。該函數的實現取決於線程所執行的工做類型,在此不顯示。