Run Loop詳解

  Run loops是線程的基礎架構部分。一個run loop就是一個事件處理循環,用來不停的調配工做以及處理輸入事件。使用run loop的目的是使你的線程在有工做的時候工做,沒有的時候休眠。html

  Run loop的管理並不徹底是自動的。你仍必須設計你的線程代碼以在適當的時候啓動run loop並正確響應輸入事件。Cocoa和CoreFundation都提供了run loop對象方便配置和管理線程的run loop。你建立的程序不須要顯示的建立run loop;每一個線程,包括程序的主線程(main thread)都有與之相應的run loop對象。可是,本身建立的次線程是須要手動運行run loop的。在carbon和cocoa程序中,程序啓動時,主線程會自行建立並運行run loop。ios

  接下來的部分將會詳細介紹run loop以及如何爲你的程序管理run loop。關於run loop對象能夠參閱sdk文檔。編程

解析Run Loopxcode

  run loop,顧名思義,就是一個循環,你的線程在這裏開始,並運行事件處理程序來響應輸入事件。你的代碼要有實現循環部分的控制語句,換言之就是要有 while或for語句。在run loop中,使用run loop對象來運行事件處理代碼:響應接收到的事件,啓動已經安裝的處理程序。緩存

  Run loop處理的輸入事件有兩種不一樣的來源:輸入源(input source)和定時源(timer source)。輸入源傳遞異步消息,一般來自於其餘線程或者程序。定時源則傳遞同步消息,在特定時間或者必定的時間間隔發生。兩種源的處理都使用程序的 某一特定處理路徑。安全

  下圖顯示了run loop的結構以及各類輸入源。輸入源傳遞異步消息給相應的處理程序,並調用runUntilDate:方法退出。定時源則直接傳遞消息給處理程序,但並不會退出run loop。架構

 

  除了處理輸入源,run loop也會生成關於run loop行爲的notification。註冊的run-loop 觀察者能夠收到這些notification,並作相應的處理。可使用Core Foundation在你的線程註冊run-loop觀察者。併發

  下面介紹run loop的組成,以及其運行的模式。同時也說起在處理程序中不一樣時間發送不一樣的notification。app

Run Loop Modes框架

  Run loop模式是全部要監視的輸入源和定時源以及要通知的註冊觀察者的集合。每次運行run loop都會指定其運行在哪一個模式下。之後,只有相應的源會被監視並容許接收他們傳遞的消息。(相似的,只有相應的觀察者會收到通知)。其餘模式關聯的源 只有在run loop運行在其模式下才會運行,不然處於暫停狀態。

  一般代碼中經過指定名字來肯定模式。Cocoa和core foundation定義了默認的以及一系列經常使用的模式,都是用字符串來標識。固然你也能夠指定字符串來自定義模式。雖然你能夠給模式指定任何名字,可是 全部的模式內容都是相同的。你必須添加輸入源,定時器或者run loop觀察者到你定義的模式中。

  經過指定模式可使得run loop在某一階段只關注感興趣的源。大多數時候,run loop都是運行在系統定義的默認模式。可是模態面板(modal panel)能夠運行在 「模態」模式下。在這種模式下,只有和模態面板相關的源能夠傳遞消息給線程。對於次線程,可使用自定義模式處理時間優先的操做,即屏蔽優先級低的源傳遞 消息。

  Note:模式區分基於事件的源而非事件的種類。例如,你不可使用模式只選擇處理鼠標按下或者鍵盤事件。你可使用模式監聽端口, 暫停定時器或者其餘對源或者run loop觀察者的處理,只要他們在當前模式下處於監聽狀態。

下圖列出了cocoa和Core Foundation預先定義的模式。

 

輸入源

  輸入源向線程發送異步消息。消息來源取決於輸入源的種類:基於端口的輸入源和自定義輸入源。基於端口的源監聽程序相應的端口,而自定義輸入源則關注自定義 的消息。至於run loop,它不關心輸入源的種類。系統會去實現兩種源供你使用。兩類輸入源的區別在於如何顯示的:基於端口的源由內核自動發送,而自定義的則須要人工從其 他線程發送。

  當你建立輸入源,你須要將其分配給run loop中的一個或多個模式。模式只會在特定事件影響監聽的源。大多數狀況下,run loop運行在默認模式下,可是你也可使其運行在自定義模式。若某一源在當前模式下不被監聽,那麼任何其生成的消息只有當run loop運行在其關聯的模式下才會被傳遞。

下面討論這幾種輸入源

基於端口的源:

  cocoa和core foundation爲使用端口相關的對象和函數建立的基於端口的源提供了內在支持。Cocoa中你從不須要直接建立輸入源。你只須要簡單的建立端口對 象,並使用NSPort的方法將端口對象加入到run loop。端口對象會處理建立以及配置輸入源。

  在core foundation,你必須手動的建立端口和源,你均可以使用端口類型(CFMachPortRef,CFMessagePortRef,CFSocketRef)來建立。

更多例子能夠看 配置基於端口的源。

自定義輸入源:

  在Core Foundation程序中,必須使用CFRunLoopSourceRef類型相關的函數來建立自定義輸入源,接着使用回調函數來配置輸入源。Core Fundation會在恰當的時候調用回調函數,處理輸入事件以及清理源。

  除了定義如何處理消息,你也必須定義源的消息傳遞機制——它運行在單獨的進程,並負責傳遞數據給源和通知源處理數據。消息傳遞機制的定義取決於你,但最好不要過於複雜。

  關於建立自定義輸入源的例子,見 定義自定義輸入源。關於自定義輸入源的信息參見CFRunLoopSource。

Cocoa Perform Selector Sources

  除了基於端口的源,Cocoa提供了能夠在任一線程執行函數(perform selector)的輸入源。和基於端口的源同樣,perform selector請求會在目標線程上序列化,減緩許多在單個線程上容易引發的同步問題。而和基於端口的源不一樣的是,perform selector執行完後會自動清除出run loop。

  當perform selector在其它線程中執行時,目標線程須有一活動中的run loop。對於你建立的線程而言,這意味着線程直到你顯示的開始run loop不然處於等待狀態。然而,因爲主線程本身啓動run loop,在程序調用applicationDidFinishlaunching:的時候你會遇到線程調用的問題。由於Run loop經過每次循環來處理全部排列的perform selector調用,而不時經過每次的循環迭代來處理perform selector。

  下面表格中列出了NSObject能夠在其它線程使用的perform selector。因爲這些方法時定義在NSObject的,你能夠在包括POSIX的全部線程中使用只要你有objc對象的訪問權。注意這些方法實際上 並無建立新的線程以運行perform selector。

定時源

  定時源在預設的時間點同步地傳遞消息。定時器時線程通知本身作某事的一種方法。例如,搜索控件可使用定時器,當用戶連續輸入的時間超過必定時間時,就開始一次搜索。這樣,用戶就能夠有足夠的時間來輸入想要搜索的關鍵字。

  儘管定時器和時間有關,但它並非實時的。和輸入源同樣,定時器也是和run loop的運行模式相關聯的。若是定時器所在的模式未被run loop監視,那麼定時器將不會開始直到run loop運行在相應的模式下。相似的,若是定時器在run loop處理某一事件時開始,定時器會一直等待直到下次run loop開始相應的處理程序。若是run loop再也不運行,那定時器也將永遠不開始。

  你能夠選擇定時器工做一次仍是定時工做。若是定時工做,定時器會基於安排好的時間而非實際時間,自動的開始。舉個例子,定時器在某一特定時間開始並設置5 秒重複,那麼定時器會在那個特定時間後5秒啓動,即便在那個特定時間定時器延時啓動了。若是定時器延遲到接下來設定的一個會多個5秒,定時器在這些時間段 中也只會啓動一次,在此以後,正常運行。(假設定時器在時間1,5,9。。。運行,若是最初延遲到7才啓動,那仍是從9,13,。。。開始)。

 

Run Loop觀察者

  源是同步或異步的傳遞消息,而run loop觀察者則是在運行run loop的時候在特定的時候開始。你可使用run loop觀察者來爲某一特定事件或是進入休眠的線程作準備。你能夠將觀察者將如下事件關聯:

   Run loop入口

   Run loop將要開始定時

   Run loop將要處理輸入源

   Run loop將要休眠

   Run loop被喚醒但又在執行喚醒事件前

   Run loop終止

  你能夠給cocoa和carbon程序隨意添加觀察者,可是若是你要定義觀察者的話就只能使用core fundation。使用CFRunLoopObserverRed類型來建立觀察者實例,它會追蹤你自定義的回調函數以及其它你感興趣的地方。

  和定時器相似,觀察者能夠只用一次或循環使用。若只用一次,那在結束的時候會移除run loop,而循環的觀察者則不會。你須要制定觀察者是一次/屢次使用。

消息的run loop順序

  每次啓動,run loop會自動處理以前未處理的消息,並通知觀察者。具體的順序,以下:

  1. 通知觀察者,run loop啓動

  2. 通知觀察者任何即將要開始的定時器

  3. 通知觀察者任何非基於端口的源即將啓動

  4. 啓動任何準備好的非基於端口的源

  5. 若是基於端口的源準備好並處於等待狀態,當即啓動;並進入步驟9。

  6. 通知觀察者線程進入休眠

  7. 將線程之於休眠直到任一下面的事件發生

  8. 某一事件到達基於端口的源

  9. 定時器啓動

  10. 設置了run loop的終止時間

  11. run loop喚醒

  12. 通知觀察者線程將被喚醒。

  13. 處理未處理的事件

  14. 若是用戶定義的定時器啓動,處理定時事件並重啓run loop。進入步驟2

  15. 若是輸入源啓動,傳遞相應的消息

  16. run loop喚醒但未終止,重啓。進入步驟2

  17. 通知觀察者run loop結束。

  由於觀察者的消息傳遞是在相應的事件發生以前,因此二者之間可能存在偏差。若是須要精確時間控制,你可使用休眠和喚醒通知以此來校對實際發生的事件。

  由於定時器和其它週期性事件那是在run loop運行後才啓動,撤銷run loop也會終止消息傳遞。典型的例子就是鼠標路徑追蹤。由於你的代碼直接獲取到消息而不是經由程序傳遞,從而不會在實際的時間開始而須使得鼠標追蹤結束並將控制權交給程序後才行。

  使用run loop對象能夠喚醒Run loop。其它消息也能夠喚醒run loop。例如,添加新的非基於端口的源到run loop從而能夠當即執行輸入源而不是等待其餘事件發生後再執行。

什麼時候使用Run Loop

  只有在爲你的程序建立次線程的時候,才須要運行run loop。對於程序的主線程而言,run loop是關鍵部分。Cocoa和carbon程序提供了運行主線程run loop的代碼同時也會自動運行run loop。IOS程序UIApplication中的run方法在程序正常啓動的時候就會啓動run loop。一樣的這部分工做在carbon程序中由RunApplicationEventLoop負責。若是你使用xcode提供的模板建立的程序,那 你永遠不須要本身去啓動run loop。

  而對於次線程,你須要判斷是否須要run loop。若是須要run loop,那麼你要負責配置run loop並啓動。你不須要在任何狀況下都去啓動run loop。好比,你使用線程去處理一個預先定義好的耗時極長的任務時,你就能夠毋需啓動run loop。Run loop只在你要和線程有交互時才須要,好比如下狀況:

   使用端口或自定義輸入源和其餘線程通訊

   使用定時器

   cocoa中使用任何performSelector

   使線程履行週期性任務

  若是決定在程序中使用run loop,那麼配置和啓動都須要本身完成。和全部線程編程同樣,你須要計劃好什麼時候退出線程。在退出前結束線程每每是比被強制關閉好的選擇。詳細的配置和推出run loop的信息見 使用run loop對象。

使用Run loop對象

  run loop對象提供了添加輸入源,定時器和觀察者以及啓動run loop的接口。每一個線程都有惟一的與之關聯的run loop對象。在cocoa中,是NSRunLoop對象;而在carbon或BSD程序中則是指向CFRunLoopRef類型的指針。

得到run loop對象

得到當前線程的run loop,能夠採用:

  • cocoa:使用NSRunLoop的currentRunLoop類方法

   使用CFRunLoopGetCurrent函數

  雖然CFRunLoopRef類型和NSRunLoop對象並不徹底等價,你仍是能夠從NSRunLoop對象中獲取CFRunLoopRef類型。你可 以使用NSRunLoop的getCFRunLoop方法,返回CFRunLoopRef類型到Core Fundation中。由於二者都指向同一個run loop,你能夠任一替換使用。

配置run loop

  在次線程啓動run loop前,你必須至少添加一類源。由於若是run loop沒有任何源須要監視的話,它會在你啓動之際立馬退出。

  此外,你也能夠添加run loop觀察者來監視run loop的不一樣執行階段。首先你能夠建立CFRunLoopObserverRef類型並使用CFRunLoopAddObserver將它添加金run loop。注意即便是cocoa程序,run loop觀察者也須要由core foundation函數建立。

  如下代碼3-1實現了添加觀察者進run loop,代碼簡單的創建了一個觀察者來監視run loop的全部活動,並將run loop的活動打印出來。

 1 - (void)threadMain
 2 {
 3     // The application uses garbage collection, so no autorelease pool is needed.
 4     NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
 5  
 6     // Create a run loop observer and attach it to the run loop.
 7     CFRunLoopObserverContext  context = {0, self, NULL, NULL, NULL};
 8     CFRunLoopObserverRef    observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
 9             kCFRunLoopAllActivities, YES, 0, &myRunLoopObserver, &context);
10  
11     if (observer)
12     {
13         CFRunLoopRef    cfLoop = [myRunLoop getCFRunLoop];
14         CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
15     }
16  
17     // Create and schedule the timer.
18     [NSTimer scheduledTimerWithTimeInterval:0.1 target:self
19                 selector:@selector(doFireTimer:) userInfo:nil repeats:YES];
20  
21     NSInteger    loopCount = 10;
22     do
23     {
24         // Run the run loop 10 times to let the timer fire.
25         [myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
26         loopCount--;
27     }
28     while (loopCount);
29 }
3-1Creating a run loop observer

  若是線程運行事件長,最好添加一個輸入源到run loop以接收消息。雖然你可使用定時器,可是定時器一旦啓動後當它失效時也會使得run loop退出。雖然定時器能夠循環使得run loop運行相對較長的時間,可是也會致使週期性的喚醒線程。與之相反,輸入源會等待某事件發生,因而線程只有當事件發生後纔會從休眠狀態喚醒。

啓動run loop

  run loop只對程序的次線程有意義,而且必須添加了一類源。若是沒有,在啓動後就會退出。有幾種啓動的方法,如:

  • 無條件的

  • 預設的時間

  • 特定的模式

  無條件的進入run loop是最簡單的選擇,但也最不提倡。由於這樣會使你的線程處在一個永久的run loop中,這樣的話你對run loop自己的控制就會很小。你能夠添加或移除源,定時器,可是隻能經過殺死進程的辦法來退出run loop。而且這樣的run loop也沒有辦法運行在自定義模式下。

  用預設時間來運行run loop是一個比較好的選擇,這樣run loop在某一事件發生或預設的事件過時時啓動。若是是事件發生,消息會被傳遞給相應的處理程序而後run loop退出。你能夠從新啓動run loop以處理下一個事件。若是是時間過時,你只需重啓run loop或使用定時器作任何的其餘工做。**

  此外,使run loop運行在特定模式也是一個比較好的選擇。模式和預設時間不是互斥的,他們能夠同時存在。模式對源的限制在run loop模式部分有詳細說明。

  Listing3-2代碼描述了線程的整個結構。代碼的關鍵是說明了run loop的基本結構。必要時,你能夠添加本身的輸入源或定時器,而後重複的啓動run loop。每次run loop返回,你要檢查是否有使線程退出的條件發生。代碼中使用了Core Foundation的run loop程序,這樣就能檢查返回結果從而判斷是否要退出。如果cocoa程序,也不須要關心返回值,你也可使用NSRunLoop的方法運行run loop(代碼見listing3-14)

 1 - (void)skeletonThreadMain
 2 {
 3     // Set up an autorelease pool here if not using garbage collection.
 4     BOOL done = NO;
 5  
 6     // Add your sources or timers to the run loop and do any other setup.
 7  
 8     do
 9     {
10         // Start the run loop but return after each source is handled.
11         SInt32    result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);
12  
13         // If a source explicitly stopped the run loop, or if there are no
14         // sources or timers, go ahead and exit.
15         if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished))
16             done = YES;
17  
18         // Check for any other exit conditions here and set the
19         // done variable as needed.
20     }
21     while (!done);
22  
23     // Clean up code here. Be sure to release any allocated autorelease pools.
24 }
Listing 3-2 Running a run loop

  由於run loop有可能迭代啓動,也就是說你可使用CFRunLoopRun,CFRunLoopRunInMode或者任一NSRunLoop的方法來啓動run loop。這樣作的時候,你可使用任何模式啓動迭代的run loop,包括被外層run loop使用的模式。

退出run loop

run loop處理事件前,有兩種方法使其退出:

  • 設置超時限定

   通知run loop中止

  若是能夠配置的話,使用第一種方法是較好的選擇。這樣,可使run loop完成全部正常操做,包括髮送消息給run loop觀察者,最後再退出。

  使用CFRunLoopStop來中止run loop也有相似的效果。Run loop也會把全部未發送的消息發送完後再退出。與設置時間的區別在於你能夠在任何狀況下中止run loop。

  儘管移除run loop的輸入源和定時器也可使run loop退出,但這並非可靠的退出run loop的辦法。一些系統程序會添加輸入源來處理必須的事件。而你的代碼未必會考慮到這些,這樣就沒有辦法從系統程序中移除,從而就沒法退出run loop。

線程安全和run loop對象

  線程是否安全取決於你使用哪一種API操縱run loop。Core Foundation中的函數一般是線程安全的能夠被任意線程調用。可是,若是你改變了run loop的配置而後須要進行某些操做,你最好仍是在run loop所在線程去處理。若是可能的話,這樣是個好習慣。

  至於Cocoa的NSRunLoop則不像Core Foundation具備與生俱來的線程安全性。你應該只在run loop所在線程改變run loop。若是添加yuan或定時器到屬於另外一個線程的run loop,程序會崩潰或發生意想不到的錯誤。

Run loop 源的配置

  下面的例子說明了若是使用cocoa和core foundation來創建不一樣類型的輸入源。

定義自定義輸入源

  遵循下列步驟來建立自定義的輸入源:

  • 輸入源要處理的信息

  • 使感興趣的客戶知道如何和輸入源交互的調度程序

  • 處理客戶發送請求的程序

  • 使輸入源失效的取消程序

  因爲你本身建立源來處理消息,實際配置設計得足夠靈活。調度,處理和取消程序是你建立你得自定義輸入源時總會須要用到得關鍵程序。可是,輸入源其餘的大部分行爲都是由其餘程序來處理。例如,由你決定數據傳輸到輸入源的機制,還有輸入源和其餘線程的通訊機制。

  下圖列舉了自定義輸入源的配置。在這個例子中,程序的主線程保持了輸入源,輸入源所需的命令緩衝區和輸入源所在的run loop的引用。當主線程有任務,須要分發給目標線程,主線程會給命令緩衝區發送命令和必須的信息,這樣活動線程就能夠開始執行任務。(由於主線程和輸入源所在線程都須訪問命令緩衝區,因此他們的操做要注意同步。)一旦命令傳送了,主線程會通知輸入源而且喚醒活動線程的run loop。而一收到喚醒命令,run loop會調用輸入源的處理部分,由它來執行命令緩衝區中相應的命令。

 

下面解釋下上圖的關鍵代碼。

定義輸入源

  定義輸入源須要使用Core Foundation來配置run loop源並把它添加到run loop。基本的函數是C函數,固然你也能夠用objc或C++來封裝操做。

  上圖中的輸入源使用了objc對象來管理命令緩衝區和run loop。Listing3-3說明了此對象的定義:RunLoopSource對象管理着命令緩衝區並以此來接收其餘線程的消息;RunLoopContext對象是一個用於傳遞RunLoopSource對象和run loop引用給程序主線程的一個容器。

 1 @interface RunLoopSource : NSObject
 2 {
 3     CFRunLoopSourceRef runLoopSource;
 4     NSMutableArray* commands;
 5 }
 6  
 7 - (id)init;
 8 - (void)addToCurrentRunLoop;
 9 - (void)invalidate;
10  
11 // Handler method
12 - (void)sourceFired;
13  
14 // Client interface for registering commands to process
15 - (void)addCommand:(NSInteger)command withData:(id)data;
16 - (void)fireAllCommandsOnRunLoop:(CFRunLoopRef)runloop;
17  
18 @end
19  
20 // These are the CFRunLoopSourceRef callback functions.
21 void RunLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFStringRef mode);
22 void RunLoopSourcePerformRoutine (void *info);
23 void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode);
24  
25 // RunLoopContext is a container object used during registration of the input source.
26 @interface RunLoopContext : NSObject
27 {
28     CFRunLoopRef        runLoop;
29     RunLoopSource*        source;
30 }
31 @property (readonly) CFRunLoopRef runLoop;
32 @property (readonly) RunLoopSource* source;
33  
34 - (id)initWithSource:(RunLoopSource*)src andLoop:(CFRunLoopRef)loop;
35 @end
Listing 3-3 The custom input source object definition

  雖然輸入源的數據定義是objc代碼,可是將源添加進run loop卻須要c的回調函數。上述函數在像Listing3-4同樣,在添加時調用。由於這個輸入源只有一個客戶(即主線程),它使用調度函數發送註冊信息給程序的代理(delegate)。當代理須要和輸入源通訊時,就可使用RunLoopContext對象實現。

1 void RunLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFStringRef mode)
2 {
3     RunLoopSource* obj = (RunLoopSource*)info;
4     AppDelegate*   del = [AppDelegate sharedAppDelegate];
5     RunLoopContext* theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl];
6  
7     [del performSelectorOnMainThread:@selector(registerSource:)
8                                 withObject:theContext waitUntilDone:NO];
9 }
Listing 3-4 Scheduling a run loop source

  一個重要的回調函數就是用來處理自定義數據。Lising3-5說明了如何調用這個回調函數。這裏只是簡單的將請求傳遞到sourceFired方法,而後繼續處理在命令緩存區的命令。

1 void RunLoopSourcePerformRoutine (void *info)
2 {
3     RunLoopSource*  obj = (RunLoopSource*)info;
4     [obj sourceFired];
5 }
Listing 3-5 Performing work in the input source

  使用CFRunLoopSourceInvalidate函數移除輸入源,系統會調用輸入源的取消程序。你能夠以此通知客戶輸入源再也不有效,客戶能夠釋放輸入源的引用。Listing3-6說明了取消回調函數的調用,其中給另外一個RunLoopContext對象發送了程序代理,通知代理去除源的引用。

1 void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode)
2 {
3     RunLoopSource* obj = (RunLoopSource*)info;
4     AppDelegate* del = [AppDelegate sharedAppDelegate];
5     RunLoopContext* theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl];
6  
7     [del performSelectorOnMainThread:@selector(removeSource:)
8                                 withObject:theContext waitUntilDone:YES];
9 }

安裝輸入源到run loop

  Listing3-7說明了RunLoopSource的init和addToCurrentRunLoop函數。Init函數建立CFRunLoopSourceRef類型,傳遞RunLoopSource對象作爲信息這樣回調函數持有對象的引用。輸入源的安裝當工做線程運行addToCurrentRunLoop方法,而後調用RunLoopSourceScheduledRoutine回調函數。一旦源被添加,線程就運行run loop監聽事件。

 1 - (id)init
 2 {
 3     CFRunLoopSourceContext    context = {0, self, NULL, NULL, NULL, NULL, NULL,
 4                                         &RunLoopSourceScheduleRoutine,
 5                                         RunLoopSourceCancelRoutine,
 6                                         RunLoopSourcePerformRoutine};
 7  
 8     runLoopSource = CFRunLoopSourceCreate(NULL, 0, &context);
 9     commands = [[NSMutableArray alloc] init];
10  
11     return self;
12 }
13  
14 - (void)addToCurrentRunLoop
15 {
16     CFRunLoopRef runLoop = CFRunLoopGetCurrent();
17     CFRunLoopAddSource(runLoop, runLoopSource, kCFRunLoopDefaultMode);
18 }
Listing 3-7 Installing the run loop source

統籌輸入源的客戶

  爲了使添加的輸入源有用,你須要處理源以及從其餘線程發送信號。輸入源的主要工做就是將與源關聯的線程休眠,直到有事件發生。這就意味着程序中的線程必須知道輸入源信息並有辦法與之 通訊。

  通知客戶關於輸入源信息的方法之一就是當輸入源安裝完成後發送註冊請求。你能夠註冊輸入源到你須要的客戶,或者經過註冊中間代理由代理將輸入源到感興趣的客戶。Listing3-8說明了程序代理定義的註冊方法以及它在RUnLoopSource對象的調度函數調用時如何運行。函數接收RUnLoopSource提供的RunLoopContext對象,而後將其膠乳源隊列。另外,也說明了源移除run loop時候的取消註冊方法。

 1 - (void)registerSource:(RunLoopContext*)sourceInfo;
 2 {
 3     [sourcesToPing addObject:sourceInfo];
 4 }
 5  
 6 - (void)removeSource:(RunLoopContext*)sourceInfo
 7 {
 8     id    objToRemove = nil;
 9  
10     for (RunLoopContext* context in sourcesToPing)
11     {
12         if ([context isEqual:sourceInfo])
13         {
14             objToRemove = context;
15             break;
16         }
17     }
18  
19     if (objToRemove)
20         [sourcesToPing removeObject:objToRemove];
21 }
Listing 3-8 Registering and removing an input source with the application delegate

通知輸入源

  客戶發送數據到輸入源後,必須發信號通知源而且喚醒run loop。發信好意味着讓run loop明白源已經作好處理消息的準備。由於信號發生的時候線程可能休眠着,你必須本身喚醒run loop。若是不這樣作的話會致使延遲處理消息。

  Listing3-9說明了RunLoopSource對象的fireCommandsOnRunLoop方法。客戶若是準備好處理加入緩衝區的命令後會運行此方法。

1 - (void)fireCommandsOnRunLoop:(CFRunLoopRef)runloop
2 {
3     CFRunLoopSourceSignal(runLoopSource);
4     CFRunLoopWakeUp(runloop);
5 }
Listing 3-9 Waking up the run loop

配置定時源

  建立定時源你所須要作的就是建立定時器並加入run loop調度。Cocoa程序中使用NSTimer類而Core Foundation中使用CFRunLoopTimerRef類型。本質上,NSTimer類是Core Foundation的簡單擴展,這樣就提供了便利的特徵,例如都能使用相同的函數建立和調配定時器。

Cocoa中可使用如下函數建立並調配:

   scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:

   scheduledTimerWithTimeInterval:invocation:repeats:

  上述方法建立了定時器並使之以默認模式添加到當前線程的run loop。你能夠本身調度定時器若是你選擇本身建立定時器並使用addTimer:forMode:方法添加到run loop。兩種方法都作了相同的事,區別在於你對定時器的控制權。若是你本身建立的話,你能夠選擇添加的模式。Listing3-10說明了若是這樣作。第一個定時器在初始化後1秒開始能夠後每隔0.1秒運行,第二個定時器則在初始化後0。2秒開始之後每隔0。2運行。

 1 NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
 2  
 3 // Create and schedule the first timer.
 4 NSDate* futureDate = [NSDate dateWithTimeIntervalSinceNow:1.0];
 5 NSTimer* myTimer = [[NSTimer alloc] initWithFireDate:futureDate
 6                         interval:0.1
 7                         target:self
 8                         selector:@selector(myDoFireTimer1:)
 9                         userInfo:nil
10                         repeats:YES];
11 [myRunLoop addTimer:myTimer forMode:NSDefaultRunLoopMode];
12  
13 // Create and schedule the second timer.
14 [NSTimer scheduledTimerWithTimeInterval:0.2
15                         target:self
16                         selector:@selector(myDoFireTimer2:)
17                         userInfo:nil
18                         repeats:YES];
Listing 3-10 Creating and scheduling timers using NSTimer

  Listing3-11是使用Core Foundation函數配置定時器的代碼。這個例子中文本結構例沒有任何用戶定義的信息,可是你可使用這個結構體傳遞任何你想傳遞的信息給定時器。關於結構體的詳細信息,參見CFRunLoopTimer。

1 CFRunLoopRef runLoop = CFRunLoopGetCurrent();
2 CFRunLoopTimerContext context = {0, NULL, NULL, NULL, NULL};
3 CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 0.1, 0.3, 0, 0,
4                                         &myCFTimerCallback, &context);
5  
6 CFRunLoopAddTimer(runLoop, timer, kCFRunLoopCommonModes);
Listing 3-11 Creating and scheduling a timer using Core Foundation

配置基於端口的輸入源

  cocoa和core foundation都提供了基於端口的對象用於線程或進程間的通訊。下面的部分說明了使用幾種不一樣的端口對象創建端口通訊。

配置NSMachPort對象

  創建和NSMachPort對象的本地鏈接,你須要建立端口對象並將之加入主線程的run loop。當運行次線程的時候,你傳遞端口對象到線程的入口點。次線程可使用相同的端口對象將消息返回給主線程。

主線程的實現代碼

  Listing3-12說明了在主線程啓動次線程的方法。由於cocoa框架提供了許多配置端口和run loop的中間步驟因此lauchThread方法比相應的core foundation版本(Listing3-17)要明顯簡短。可是兩種方法的本質幾乎是同樣的,惟一的區別就是在cocoa中直接發送了NSPort對象而不是發送本地端口名。

 1 - (void)launchThread
 2 {
 3     NSPort* myPort = [NSMachPort port];
 4     if (myPort)
 5     {
 6         // This class handles incoming port messages.
 7         [myPort setDelegate:self];
 8  
 9         // Install the port as an input source on the current run loop.
10         [[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];
11  
12         // Detach the thread. Let the worker release the port.
13         [NSThread detachNewThreadSelector:@selector(LaunchThreadWithPort:)
14                toTarget:[MyWorkerClass class] withObject:myPort];
15     }
16 }
Listing 3-12 Main thread launch method

  爲了創建線程間的雙向通訊,你須要在簽到消息中從活動線程發送本身的本地端口到主線程。接收到簽到消息後你的主線程就能夠知道次線程運行正常而且提供了發送消息給次線程的方法。

  Listing3-13說明了活動線程的handlePortMessage:方法,當由數據到達線程的本地端口,次方法調用。當簽到消息收到後,此方法能夠直接獲取到次線程的端口並保存下來以作後續之用。

 1 #define kCheckinMessage 100
 2  
 3 // Handle responses from the worker thread.
 4 - (void)handlePortMessage:(NSPortMessage *)portMessage
 5 {
 6     unsigned int message = [portMessage msgid];
 7     NSPort* distantPort = nil;
 8  
 9     if (message == kCheckinMessage)
10     {
11         // Get the worker thread’s communications port.
12         distantPort = [portMessage sendPort];
13  
14         // Retain and save the worker port for later use.
15         [self storeDistantPort:distantPort];
16     }
17     else
18     {
19         // Handle other messages.
20     }
21 }
Listing 3-13 Handling Mach port messages

次線程代碼實現

  對於次線程,你必須配置線程並使用特定的端口以發送消息返回至活動線程。

  Listing3-14說明了如何創建活動線程。建立了自動釋放池後,緊接着創建了活動對象驅動線程運行。活動對象的sendCheckinMessage:方法建立了本地端口併發送簽到消息回主線程。

 1 +(void)LaunchThreadWithPort:(id)inData
 2 {
 3     NSAutoreleasePool*  pool = [[NSAutoreleasePool alloc] init];
 4  
 5     // Set up the connection between this thread and the main thread.
 6     NSPort* distantPort = (NSPort*)inData;
 7  
 8     MyWorkerClass*  workerObj = [[self alloc] init];
 9     [workerObj sendCheckinMessage:distantPort];
10     [distantPort release];
11  
12     // Let the run loop process things.
13     do
14     {
15         [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
16                             beforeDate:[NSDate distantFuture]];
17     }
18     while (![workerObj shouldExit]);
19  
20     [workerObj release];
21     [pool release];
22 }
Listing 3-14 Launching the worker thread using Mach ports

  使用NSMachPort,本地和遠程線程都使用相同的端口對象救星線程的單邊通訊。就是說,一個線程的本地端口對象是另外一個線程的遠程端口對象。

  Listing3-15說明了次線程的簽到程序,它創建了本地端口用於通訊,而後發送簽到消息。它使用LaunchThreadWithPort:方法中收到的端口對象作爲目標消息。

 1 // Worker thread check-in method
 2 - (void)sendCheckinMessage:(NSPort*)outPort
 3 {
 4     // Retain and save the remote port for future use.
 5     [self setRemotePort:outPort];
 6  
 7     // Create and configure the worker thread port.
 8     NSPort* myPort = [NSMachPort port];
 9     [myPort setDelegate:self];
10     [[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];
11  
12     // Create the check-in message.
13     NSPortMessage* messageObj = [[NSPortMessage alloc] initWithSendPort:outPort
14                                          receivePort:myPort components:nil];
15  
16     if (messageObj)
17     {
18         // Finish configuring the message and send it immediately.
19         [messageObj setMsgid:kCheckinMessage];
20         [messageObj sendBeforeDate:[NSDate date]];
21     }
22 }
Listing 3-15 Sending the check-in message using Mach ports

配置NSMessagePort對象

創建和NSMeaasgePort的本地鏈接,你將不能隨意在線程間傳遞端口對象。遠程消息端口必須經過名字來得到。在cocoa中這須要你給本地端口註冊名字而後將名字傳遞到遠程線程這樣遠程線程能夠得到合適的端口對象用於通訊。Listing3-16說明了這些步驟。

 1 NSPort* localPort = [[[NSMessagePort alloc] init] retain];
 2  
 3 // Configure the object and add it to the current run loop.
 4 [localPort setDelegate:self];
 5 [[NSRunLoop currentRunLoop] addPort:localPort forMode:NSDefaultRunLoopMode];
 6  
 7 // Register the port using a specific name. The name must be unique.
 8 NSString* localPortName = [NSString stringWithFormat:@"MyPortName"];
 9 [[NSMessagePortNameServer sharedInstance] registerPort:localPort
10                      name:localPortName];
Listing 3-16 Registering a message port

在core foundation配置基於端口的源

這部分說明了在corefoundation創建程序主線程和活動線程的雙邊通道。

  Listing3-17是程序主線程啓動活動線程:第一是創建CFMessagePortRef類型監聽活動線程的消息。活動線程須要端口的名字來創建鏈接,這樣線程名就回從活動線程的入口傳入。端口名在當前的用戶菜單下須始終惟一不然會出錯。

 1 #define kThreadStackSize        (8 *4096)
 2  
 3 OSStatus MySpawnThread()
 4 {
 5     // Create a local port for receiving responses.
 6     CFStringRef myPortName;
 7     CFMessagePortRef myPort;
 8     CFRunLoopSourceRef rlSource;
 9     CFMessagePortContext context = {0, NULL, NULL, NULL, NULL};
10     Boolean shouldFreeInfo;
11  
12     // Create a string with the port name.
13     myPortName = CFStringCreateWithFormat(NULL, NULL, CFSTR("com.myapp.MainThread"));
14  
15     // Create the port.
16     myPort = CFMessagePortCreateLocal(NULL,
17                 myPortName,
18                 &MainThreadResponseHandler,
19                 &context,
20                 &shouldFreeInfo);
21  
22     if (myPort != NULL)
23     {
24         // The port was successfully created.
25         // Now create a run loop source for it.
26         rlSource = CFMessagePortCreateRunLoopSource(NULL, myPort, 0);
27  
28         if (rlSource)
29         {
30             // Add the source to the current run loop.
31             CFRunLoopAddSource(CFRunLoopGetCurrent(), rlSource, kCFRunLoopDefaultMode);
32  
33             // Once installed, these can be freed.
34             CFRelease(myPort);
35             CFRelease(rlSource);
36         }
37     }
38  
39     // Create the thread and continue processing.
40     MPTaskID        taskID;
41     return(MPCreateTask(&ServerThreadEntryPoint,
42                     (void*)myPortName,
43                     kThreadStackSize,
44                     NULL,
45                     NULL,
46                     NULL,
47                     0,
48                     &taskID));
49 }
Listing 3-17 Attaching a Core Foundation message port to a new thread

  端口創建線程啓動後,主線程繼續執行,邊等到線程簽到。到收到簽到消息後,主線程使用MainThreadResponsehandler來分發消息,如Listing3-18。這個函數提取活動線程的端口名,並建立用於將來通訊的管道。

 1 #define kCheckinMessage 100
 2  
 3 // Main thread port message handler
 4 CFDataRef MainThreadResponseHandler(CFMessagePortRef local,
 5                     SInt32 msgid,
 6                     CFDataRef data,
 7                     void* info)
 8 {
 9     if (msgid == kCheckinMessage)
10     {
11         CFMessagePortRef messagePort;
12         CFStringRef threadPortName;
13         CFIndex bufferLength = CFDataGetLength(data);
14         UInt8* buffer = CFAllocatorAllocate(NULL, bufferLength, 0);
15  
16         CFDataGetBytes(data, CFRangeMake(0, bufferLength), buffer);
17         threadPortName = CFStringCreateWithBytes (NULL, buffer, bufferLength, kCFStringEncodingASCII, FALSE);
18  
19         // You must obtain a remote message port by name.
20         messagePort = CFMessagePortCreateRemote(NULL, (CFStringRef)threadPortName);
21  
22         if (messagePort)
23         {
24             // Retain and save the thread’s comm port for future reference.
25             AddPortToListOfActiveThreads(messagePort);
26  
27             // Since the port is retained by the previous function, release
28             // it here.
29             CFRelease(messagePort);
30         }
31  
32         // Clean up.
33         CFRelease(threadPortName);
34         CFAllocatorDeallocate(NULL, buffer);
35     }
36     else
37     {
38         // Process other messages.
39     }
40  
41     return NULL;
42 }
Listing 3-18 Receiving the checkin message

  主線程配置好後,剩下的惟一事情是讓新建立的工做線程建立本身的端口而後簽到。Listing3-19顯示了工做線程的入口函數。函數獲取了端口名並使用它來建立和主線程的遠程鏈接。而後這個函數建立本身的本地斷後,安裝到線程的run loop,最後連同本地端口名一塊兒發回主線程簽到。

 1 OSStatus ServerThreadEntryPoint(void* param)
 2 {
 3     // Create the remote port to the main thread.
 4     CFMessagePortRef mainThreadPort;
 5     CFStringRef portName = (CFStringRef)param;
 6  
 7     mainThreadPort = CFMessagePortCreateRemote(NULL, portName);
 8  
 9     // Free the string that was passed in param.
10     CFRelease(portName);
11  
12     // Create a port for the worker thread.
13     CFStringRef myPortName = CFStringCreateWithFormat(NULL, NULL, CFSTR("com.MyApp.Thread-%d"), MPCurrentTaskID());
14  
15     // Store the port in this thread’s context info for later reference.
16     CFMessagePortContext context = {0, mainThreadPort, NULL, NULL, NULL};
17     Boolean shouldFreeInfo;
18     Boolean shouldAbort = TRUE;
19  
20     CFMessagePortRef myPort = CFMessagePortCreateLocal(NULL,
21                 myPortName,
22                 &ProcessClientRequest,
23                 &context,
24                 &shouldFreeInfo);
25  
26     if (shouldFreeInfo)
27     {
28         // Couldn't create a local port, so kill the thread.
29         MPExit(0);
30     }
31  
32     CFRunLoopSourceRef rlSource = CFMessagePortCreateRunLoopSource(NULL, myPort, 0);
33     if (!rlSource)
34     {
35         // Couldn't create a local port, so kill the thread.
36         MPExit(0);
37     }
38  
39     // Add the source to the current run loop.
40     CFRunLoopAddSource(CFRunLoopGetCurrent(), rlSource, kCFRunLoopDefaultMode);
41  
42     // Once installed, these can be freed.
43     CFRelease(myPort);
44     CFRelease(rlSource);
45  
46     // Package up the port name and send the check-in message.
47     CFDataRef returnData = nil;
48     CFDataRef outData;
49     CFIndex stringLength = CFStringGetLength(myPortName);
50     UInt8* buffer = CFAllocatorAllocate(NULL, stringLength, 0);
51  
52     CFStringGetBytes(myPortName,
53                 CFRangeMake(0,stringLength),
54                 kCFStringEncodingASCII,
55                 0,
56                 FALSE,
57                 buffer,
58                 stringLength,
59                 NULL);
60  
61     outData = CFDataCreate(NULL, buffer, stringLength);
62  
63     CFMessagePortSendRequest(mainThreadPort, kCheckinMessage, outData, 0.1, 0.0, NULL, NULL);
64  
65     // Clean up thread data structures.
66     CFRelease(outData);
67     CFAllocatorDeallocate(NULL, buffer);
68  
69     // Enter the run loop.
70     CFRunLoopRun();
71 }
Listing 3-19 Setting up the thread structures

  一旦線程啓動run loop,全部發送到線程端口的事件都會由ProcessClientRequest函數處理。函數的具體實現依賴於線程的工做方式,這裏就不舉例了。

翻譯完結,有不少術語不知道怎麼對應,謝謝各位能看到此處。

最後,總結一下:

  普通作ios應用的都不會直接接觸到run loop,可是若是是作線程間通訊或程序通訊之類的就須要好好理解和掌握run loop。

 

參考文章:

http://www.cnblogs.com/scorpiozj/

相關文章
相關標籤/搜索