Run loops是線程相關的一些基本東西。一個run loop是一個處理消息的循環。用來處理計劃任務或者收到的事件。run loop的作用是在有事做的時候保持線程繁忙,沒事的時候讓線程掛起。
Run loop的管理并不是完全自動的。你仍然需要設計代碼來在合適的時候啟動run loop來相應事件。Cocoa和Core Foundation都提供了run loop對象來配置和管理run loop。程序并不需要創建這些對象,每個線程,包括主線程都有一個對應的run loop對象。只有非主線程需要明確的啟動它的run loop。自動啟動主線程的run loop是app框架啟動流程的一部分。
下面會介紹一下run loop以及如何配置它。
?
Run Loop 詳解
Run loop正如它名字所說的一樣。是線程進入的一個環,用來處理接收和處理事件。你需要寫代碼來控制run loop實際的循環,也就是說,你需要提供驅動run loop的while或者for循環。在循環中,使用run loop對象來處理事件,接收事件以及調用對應的處理程序。
run loop接收兩種源。輸入源傳遞異步的消息,通常是其他線程或其他程序發送過來的。定時器源傳遞同步事件,在一個計劃的時間或重復的時間間隔產生。兩種類型都使用程序指定的處理程序來處理事件。
下面的圖展示了run loop和它的消息源的概念。輸入源傳遞異步事件給對應的處理程序并且導致runUntilDate:方法退出。Timer發送事件給對應的處理程序但是不會導致run loop退出
?
另外,run loop有時候會發出廣播。注冊run-loop observers可以接收到這些廣播然后來在線程上做你想做的事。可以使用Core Foundation來在線程上設置run-loop observers
?
Run Loop Modes
一個run loop mode是一個需要監控和處理的輸入源和定時器的集合。每次運行run loop,都可以設置一個類型來運行。在這種情況下,只有和這種類型相關的事件才會被接收到。(也就是說,只有和這種類型相關的事件才會通知run loop的執行程序。)其他類型相關的源會掛起直到有對應的類型來接收它。
代碼中使用名字來標識類型。Cocoa和Core Foundation都定義了一個默認類型以及其他幾個通常用到的類型,也是通過字符串來標識他們的。你可以給類型名字指定一個字符串來自定義類型。雖然你可以自定義任何名字,但是類型的內容并不是任意的。你必須要添加一個或多個輸入源,定時器或run-loop observer來讓他們有意義。
使用類型來過濾run loop的事件。大多數時候,都會運行系統默認的類型。modal panel可能會使用"modal"類型。在這種類型下,只有和modal pannel相關的事件會發送。對于其他線程,可以使用自定義的類型來過濾低優先級的源。
提示:類型是根據事件源的類型,而不是事件的類型。比如,你不僅僅需要只需要鼠標按下或鍵盤事件。可能還需要監聽端口,定時器掛起,或源的改變等。
下面是Cocoa和Core Foundation預定義的一些類型。
Mode | 名字 | 描述 |
Defaule |
NSDefaultRunLoopMode(Cocoa) kCFRunLoopDefaultMode(Core Foundation) |
默認類型是大部分操作用到的。大多數時候都使用這種類型來啟動run loop以及配置輸入源。 |
Connection | NSConnectionReplyMode(Cocoa) |
Cocoa使用這種類型來監測NSCOnnection對象返回。很少用到這種類型。 |
Modal | NSModalPannelRunLoopMode(Cocoa) |
Cocoa使用這種類型來標識modal pannels相關的事件。 |
Event tracking | NSEventTrackingRunLoopMode(Cocoa) |
Cocoa使用這種類型來監測鼠標拖動以及其他類型的用戶界面操作追蹤。 |
Common modes |
NSRunLoopCommonModes(Cocoa) kCFRunLoopCommonModes(Core Foundation) |
這是常用類型的集合。指定一個輸入源和這個類型相關也就是指定它和這個集合的類型相關。對于Cocoa程序,這個集合包括default,modal,以及event tracking類型。Core Foundation只包括default類型。可以使用CFRunLoopAddCommonMode方法來添加自定義類型到這個集合 |
?
輸入源
輸入源異步的發送消息到線程。事件的源取決于輸入源,總體上有兩種類型。基于Port的源模擬程序的port源,自定義源模擬自定義事件。不過run loop關心的并不是基于port或自定義事件。系統基本上兩種都會實現。唯一的不同是他們如何發出的。基于port的是自動由內核發出的,自定義的源是由其他線程發出的。
創建輸入源時,可以指定一個或多個run loop類型。類型決定了輸入源在什么時候被監測到。大多數時候,run loop在默認類型下運行,不過也可以指定類型。如果輸入源不是當前監測的類型,任何產生的事件都會掛起,直到有對應的類型能接收它。
下面介紹一些輸入源
?
Port-Based Sources
Cocoa和Core Foundation提供了創建port-based源的相應對象和方法。例如,在Cocoa,基本上不用直接創建輸入源。只需要創建一個port對象然后使用NSPort的方法把它加到run loop中。port對象會處理創建和配置輸入源的事情。
?
自定義輸入源
創建自定義輸入源,需要使用Core Foundation中CFRunLoopSourceRef相關的方法。可以給輸入源配置幾個回調方法。Core Foundation會在不同點回調這些方法來配置輸入源,處理事件,以及在從run loop移除時銷毀它。
另外如果要定義收到事件的行為的話,需要定義事件的分發機制。這部分在另一個線程上運行,它負責給輸入源提供數據并且在數據準備好后通知它。事件的分發之際又你決定,但是不要弄的太復雜。
?
Cocoa Perform Selector Sources
對于port-based sources,Cocoa定義了一個自定義的源來在任何線程上執行方法。和port-based源相似的是,執行方法的請求在目標線程上被序列化,這樣可以減輕多個方法在同一個線程上被調用的同步問題。和port-based源不同的是,它執行后會從run loop把自己移除掉。
在另一個線程上執行方法時,目標線程必須要有一個活著的run loop。對于你創建的線程,這意味著它會等到啟動run loop時才執行。因為主線程會自動啟動它的run loop,因此在程序調用applicationDidFinishLaunching:后就可以開始調用這個方法了。run loop會一次調用所有計劃的方法,而不是一個循環調用一個。
下面列出了NSObject在另一個線程上執行方法的方法。由于是在NSObject中定義的,所以可以在任何能訪問Objective-C對象的線程中調用,包括POSIX線程。這些方法不會創建新線程來執行他們。
方法 | 描述 |
performSelectorOnMainThread:withObject:waitUntilDone: performSelectorOnMainThread:withObject:waitUntilDone:modes: |
在主線程的下一個run loop中執行指定的方法。這個方法可以阻塞當前線程直到指定的方法執行完 |
performSelector:onThread:withObject:waitUntilDone: performSelector:onThread:withObject:waitUntilDone:modes: |
在指定的線程上執行指定的方法。可以阻塞房前線程直到指定的方法執行完 |
performSelector:withObject:afterDelay: performSelector:withObject:afterDelay:inModes: |
在當前線程的下一個run loop中執行指定方法并且可以設置延時時間。由于等到下個run loop才會執行,所以這些方法默認的有一個最小的延時時間。隊列中的多個方法會根據隊列中的位置一個一個的執行。 |
cancelPreviousPerformRequestsWithTarget: cancelPreviousPerformRequestsWithTarget:selector:object: |
可以取消使用 performSelector:withObject:afterDelay: 或 performSelector:withObject:afterDelay:inModes: 發送給當前線程的消息。 |
?
定時器源
定時器源會在預設的時間給線程同步的發送事件。定時器是線程通知自己做一些事情的一種方法。例如,搜索框可以用定時器來實現用戶輸入內容后自動搜索。延時可以讓用戶在開始搜索前輸入想輸入的內容。
雖然它產生基于時間的消息,定時器并不是實時的。和輸入源一樣,定時器也是和run loop指定的類型相關。如果定時器不在當前run loop監測的類型中,它會一直等到支持定時器的run loop執行時才會觸發。同樣的,如果定時器在run loop執行的過程中觸發了,定時器會等到下一個run loop才能實行相應的方法。如果run loop沒有運行,定時器就根本不會觸發。
?
Run Loop Observers
和源不同,源是在同步或異步事件產生是觸發,run loop observers在run loop指定的特殊點觸發。可以使用run loop observsers來讓線程準備處理事件或讓線程準備掛起。可以把run loop observers和下面的事件關聯起來:
- run loop的入口
- run loop將要開始執行定時器
- run loop將要執行一個輸入源
- run loop將要掛起
- run loop被喚醒
- run loop退出
可以使用Core Foundation來添加run loop監聽者。要創建一個run loop監聽者,需要創建一個CFRunLoopObserverRef實例。這個類型會追蹤指定的回調方法以及感興趣的事件。
和定時器相似,run loop監聽者可以被使用一次或重復使用。一次性的監聽者會在觸發后從run loop移除,重復使用的會保留。使用一次還是重復使用是在創建時指定的。
?
Run Loop事件順序
每次運行時,線程的run loop會執行預定義的事件并且給每個監聽者發廣播。調用的順序時固定的:
- 通知監聽者run loop進入了
- 通知監聽者任何準備好的定時器將要觸發
- 通知監聽者任何非基于port的輸入源將要觸發
- 觸發任何非基于port的事件
- 如果有任何基于port事件將要觸發,處理事件,然后到第9步
- 通知監聽者線程將要掛起
-
把線程掛起直到下面的事件之一觸發
- 一個基于port的消息觸發
- 定義定時器觸發
- run loop設置的超時時間到了
- run loop被明確的喚醒
- 通知監聽者線程被喚醒
-
處理需要處理的事件
- 如果一個用戶定義的定時器觸發,執行定時器然后重啟消息循環。跳轉到第2步
- 如果一個事件源觸發,分發事件
- 如果run loop被明確的喚醒但是還沒有超時,重啟消息循環。跳轉到第2步
- 通知所有的監聽者run loop退出
由于定時器和輸入源的監聽者廣播是在事件執行前發出的,廣播的事件和真實執行的事件可能會有時間差。如果這個時間差很重要,可以使用sleep和awake-from-sleep廣播來修正真實時間。
由于定時器和其他周期性的事件是在run loop運行時分發的,破壞run loop會破會消息分發。
?
什么時候應該使用Run Loop
程序中只有明確的需要使用另外一個線程時才會需要run loop。主線程的run loop是程序的基礎部分之一。所以,app的框架提供了運行主線程以及自動啟動run loop的代碼。UIApplication的run方法啟動了主循環。如果使用Xcode工程模版創建程序,根本步需要調用這些方法。
對于輔助線程,你需要決定是否需要run loop, 如果需要的話需要自己配置并啟動它。有些時候完全步需要run loop。比如,創建一個線程來執行一個長時間或指定好的任務,這時候根本不需要啟動run loop。Run loop在需要很多線程間交互的時候使用。比如,需要做下面的事情時:
- 使用基于port或自定義的源來進行線程間通訊
- 在線程上使用定時器
- 在Cocoa程序中使用performSelector...方法
- 執行周期性的任務
如果選擇使用run loop,配置和使用是相對簡單的。在整個多線程編程過程中,最好是計劃好需要哪些輔助線程。線程最好是讓它正常結束而不是強行中止。
?
使用Run Loop對象
run loop對象提供了添加輸入源,定時器,run-loop obervers以及運行它的接口。每個線程都有一個對應的run loop對象。在Cocoa中,這個對象是一個NSRunLoop類實例。在底層程序中,它是指向CFRunLoopRef的指針。
?
獲取Run Loop對象
要獲取當前線程的run loop對象,可以使用下面的方法:
- 在Cocoa程序中,使用NSRunLoop的currentRunLoop方法來獲得NSRunLoop對象。
- 使用CFRunLoopGetCurrent方法。
雖然他們不是完全相同的,但是也可以通過NSRunLoop對象獲得CFRunLoopRef。NSRunLoop類定義了一個getCGRunLoop方法來返回一個CGRunLoopRef類型。由于兩種對象指向同一個run loop,因此NSRunLoop對象和CFRunLoopRef可以混合使用。
?
配置Run Loop
在輔助線程中使用run loop之前,需要至少加入一個輸入源或定時器。如果run loop沒有東西要監控,它運行時會立刻退出。
除了添加源以外,也可以添加run loop監聽者來監測run loop運行的狀態。創建run loop監聽者,需要創建一個CFRunLoopObserverRef類型,然后使用CGRunLoopAddObserver方法添加到run loop。就算是Cocoa程序,run loop監聽者也需要使用Core Foundation來創建。
下面展示了創建run loop監聽者的主要程序。
- ( void )threadMain { // The application uses garbage collection, so no autorelease pool is needed. NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop]; // Create a run loop observer and attach it to the run loop. CFRunLoopObserverContext context = { 0 , self, NULL, NULL, NULL}; CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0 , &myRunLoopObserver, & context); if (observer) { CFRunLoopRef cfLoop = [myRunLoop getCFRunLoop]; CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode); } // Create and schedule the timer. [NSTimer scheduledTimerWithTimeInterval: 0.1 target:self selector:@selector(doFireTimer:) userInfo:nil repeats:YES]; NSInteger loopCount = 10 ; do { // Run the run loop 10 times to let the timer fire. [myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow: 1 ]]; loopCount -- ; } while (loopCount); }
如果是配置長時間存活的線程,最好是添加一個輸入源來接收消息。雖然可以只添加一個定時器,一旦定時器觸發后,如果定時器無效了,就會導致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來接收消息。每次run loop返回時,查看是否到達了結束的條件。
- ( void )skeletonThreadMain { // Set up an autorelease pool here if not using garbage collection. BOOL done = NO; // Add your sources or timers to the run loop and do any other setup. do { // Start the run loop but return after each source is handled. SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10 , YES); // If a source explicitly stopped the run loop, or if there are no // sources or timers, go ahead and exit. if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished)) done = YES; // Check for any other exit conditions here and set the // done variable as needed. } while (! done); // Clean up code here. Be sure to release any allocated autorelease pools. }
run loop是可以遞歸的使用的。也就是說,可以在輸入源或定時器的處理程序中調用CFRunLoopRun,CFRunLoopRunInMode等方法。
?
退出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對象
線程安全取決于你使用什么API來維護run loop。Core Foundation中的方法整體上都是線程安全的并且可以在任何線程調用。如果是做改變run loop的配置的操作,最好還是在run loop所屬的線程做比較好。
Cocoa的NSRunLoop類并沒有繼承Core Foundation的線程安全部分。如果是使用NSRunLoop類來修改run loop,應該在run loop所屬的線程上做。在另一個線程上給run loop添加輸入源或定時器可能會導致crash或其他異常。
?
配置Run Loop源
自定義輸入源
創建自定義源包括下面一些內容:
- 輸入源需要執行的信息
- 感興趣的客戶如何與輸入源交互
- 處理客戶請求的處理程序
- 取消輸入源的方法
- 由于是自定義的輸入源來處理自定義的信息,所有實際的配置就很靈活了。調度,處理,取消流程是自定義源的主要流程。大多數其他行為都在這幾個方法之外。比如,什么時候傳遞數據什么時候和其他線程交互由你決定。
下面的圖展示了一個簡單的自定義源。程序的主線程維護了一個輸入源的引用,輸入源的命令緩沖區,以及輸入源所在的run loop。當主線程有工作交給工作線程時,它把命令和所需的數據一起發到命令緩沖區來讓工作線程開始工作。(由于主線程和工作線程都能訪問命令緩沖區,所以訪問必須是同步的。)命令發送之后,主線程會給輸入源發送一個信號來喚醒工作線程的run loop。當收到喚醒命令后,run loop調用輸入源的處理程序來處理命令緩沖區中的命令。
?
定義輸入源
定義輸入源需要使用Core Foundation來配置以及和run loop交互。雖然基本的處理程序是基于C的方法,也可以使用Objective-C或C++來封裝。
下面展示了一個輸入源的定義。RunLoopSource對象管理一個命令緩沖區并且用它來接收其他線程的消息。也展示了一個RunLoopContext對象的定義,它只是一個把RunLoopSource對象和run loop對象的引用傳遞給主線程的容器。
@interface RunLoopSource : NSObject { CFRunLoopSourceRef runLoopSource; NSMutableArray * commands; } - ( id )init; - ( void )addToCurrentRunLoop; - ( void )invalidate; // Handler method - ( void )sourceFired; // Client interface for registering commands to process - ( void )addCommand:(NSInteger)command withData:( id )data; - ( void )fireAllCommandsOnRunLoop:(CFRunLoopRef)runloop; @end // These are the CFRunLoopSourceRef callback functions. void RunLoopSourceScheduleRoutine ( void * info, CFRunLoopRef rl, CFStringRef mode); void RunLoopSourcePerformRoutine ( void * info); void RunLoopSourceCancelRoutine ( void * info, CFRunLoopRef rl, CFStringRef mode); // RunLoopContext is a container object used during registration of the input source. @interface RunLoopContext : NSObject { CFRunLoopRef runLoop; RunLoopSource * source; } @property ( readonly ) CFRunLoopRef runLoop; @property ( readonly ) RunLoopSource* source; - ( id )initWithSource:(RunLoopSource* )src andLoop:(CFRunLoopRef)loop; @end
雖然使用Objectice-C代碼管理輸入源的自定義數據,把輸入源和run loop關聯仍然需要c回調方法。第一個被調用的方法是把它和run loop關聯起來。由于這個輸入源只有一個客戶端(主線程),它調用計劃方來發送消息來注冊到線程上。當代理需要和輸入源通訊時,使用RunLoopContext對象中的信息就可以了。
void RunLoopSourceScheduleRoutine ( void * info, CFRunLoopRef rl, CFStringRef mode) { RunLoopSource * obj = (RunLoopSource* )info; AppDelegate * del = [AppDelegate sharedAppDelegate]; RunLoopContext * theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl]; [del performSelectorOnMainThread:@selector(registerSource:) withObject:theContext waitUntilDone:NO]; }
最重要的回調過程之一是,當輸入源觸發時處理自定義的數據。下面展示了處理RunLoopSource對象相關的回調。這個方法只是簡單的把請求轉發給sourceFired方法,它會執行命令緩沖區中的命令。
void RunLoopSourcePerformRoutine ( void * info) { RunLoopSource * obj = (RunLoopSource* )info; [obj sourceFired]; }
如果你有調用CFRunLoopSourceInvalidate方法來移除輸入源,系統會調用輸入源的取消操作。可以在這個時候通知客戶端將要無效,客戶端需要移除對它的引用。下面展示了RunLoopSource對象注冊的取消回調方法。這個方法發送RunLoopContext對象給程序的代理,這次時通知他們移除引用
void RunLoopSourceCancelRoutine ( void * info, CFRunLoopRef rl, CFStringRef mode) { RunLoopSource * obj = (RunLoopSource* )info; AppDelegate * del = [AppDelegate sharedAppDelegate]; RunLoopContext * theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl]; [del performSelectorOnMainThread:@selector(removeSource:) withObject:theContext waitUntilDone:YES]; }
?
在Run Loop上配置輸入源
下面是RunLoopSource類的init和addToCurrentRunLoop方法。init方法創建一個CGRunLoopSourceRef類型是實際被關聯到run loop的。它返回它自己也就是RunLoopSource對象,這樣外面可以有一個指向對象的指針。添加到線程的工作直到工作線程調用addToCurrentRUnLoop方法后才會生效,那時候RunLoopSourceScheduleRoutine回調方法會被調用。只要這個源加到run loop上之后,線程就可以運行run loop來等待消息了
- ( id )init { CFRunLoopSourceContext context = { 0 , self, NULL, NULL, NULL, NULL, NULL, & RunLoopSourceScheduleRoutine, RunLoopSourceCancelRoutine, RunLoopSourcePerformRoutine}; runLoopSource = CFRunLoopSourceCreate(NULL, 0 , & context); commands = [[NSMutableArray alloc] init]; return self; } - ( void )addToCurrentRunLoop { CFRunLoopRef runLoop = CFRunLoopGetCurrent(); CFRunLoopAddSource(runLoop, runLoopSource, kCFRunLoopDefaultMode); }
?
客戶端和輸入源對應
想要輸入源真的有用的話,需要維護它并且用它向其他線程發送信號。輸入源的主要功能是讓相關的線程掛起,直到他們有事要做的時候在喚醒。所以就需要讓其他線程直到這個輸入源并且有一個和它通訊的方式。
一種讓客戶端知道輸入源的方法是在第一個裝載在run loop上時發送注冊請求。可以注冊任意多的你想要的客戶端,或者這冊一個核心的,然后由它把消息轉給其他的。下面展示一個定義在程序回調中的注冊方法(它在RunLoopSource對象的計劃方法中被調用了)。這個方法接收到一個RunLoopContext對象然后加入到列表中。這里也展示了如何注銷它
- ( void )registerSource:(RunLoopContext* )sourceInfo; { [sourcesToPing addObject:sourceInfo]; } - ( void )removeSource:(RunLoopContext* )sourceInfo { id objToRemove = nil; for (RunLoopContext* context in sourcesToPing) { if ([context isEqual:sourceInfo]) { objToRemove = context; break ; } } if (objToRemove) [sourcesToPing removeObject:objToRemove]; }
?
輸入源發送信號
在處理好輸入源的數據后,客戶端就可以發送信號來喚醒run loop了。輸入源發信號可以讓run loop知道可以準備好執行了。因為有可能在收到信號時線程是掛起狀態,所以每次都需要明確的喚醒run loop。否則可能會導致輸入源延遲執行。
下面展示了RunLoopSource的fireCommandsOnRunLoop方法。客戶端把命令加入到命令緩沖區并且準備好執行后會調用它。
- ( void )fireCommandsOnRunLoop:(CFRunLoopRef)runloop { CFRunLoopSourceSignal(runLoopSource); CFRunLoopWakeUp(runloop); }
注意:不要嘗試去處理SIGHUP或其他線程級別的信號。Core Foundation喚醒run loop的信號不是信號安全的,不應該在你的程序中處理。
?
配置定時器源
要創建一個定時器源,只需要創建一個定時器對象然后加到run loop上。在Cocoa中,可以使用NSTimer類來創建定時器對象,在Core Foundation中可以使用CFRunLoopTimerRef。實際上NSTimer類是Core Foundation的擴展,提供了一下更方便的方法,比如創建以及添加到線程。
在Cocoa中,可以通過下面的方法創建定時器
- scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
- scheduledTimerWithTimeInterval:invocation:repeats:
這些方法會創建一個定時器然后以默認類型(NSDefaultRunLoopMode)添加到當前線程的run loop。也可以手動創建一個NSTImer對象,然后調用NSRunLoop的addTimer:forMode:方法添加到run loop上。兩種方法本質上做了相同的事,但是可以讓你在不同層面上控制定時器的配置。比如,如果手動的創建定時器然后添加到run loop,這樣可以使用除了默認類型以外的類型。下面展示了兩種方法。第一個定時器延時1秒后每0.1秒觸發一次。第二個定時器0.2秒后每0.2秒觸發一次。
NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop]; // Create and schedule the first timer. NSDate* futureDate = [NSDate dateWithTimeIntervalSinceNow: 1.0 ]; NSTimer * myTimer = [[NSTimer alloc] initWithFireDate:futureDate interval: 0.1 target:self selector:@selector(myDoFireTimer1:) userInfo:nil repeats:YES]; [myRunLoop addTimer:myTimer forMode:NSDefaultRunLoopMode]; // Create and schedule the second timer. [NSTimer scheduledTimerWithTimeInterval: 0.2 target:self selector:@selector(myDoFireTimer2:) userInfo:nil repeats:YES];
下面展示了使用Core Foundation方法來配置定時器。雖然下面沒有傳遞任何用戶定義的信息,但是你可以使用這個數據結構傳遞你想傳遞的任何數據
CFRunLoopRef runLoop = CFRunLoopGetCurrent(); CFRunLoopTimerContext context = { 0 , NULL, NULL, NULL, NULL}; CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 0.1 , 0.3 , 0 , 0 , &myCFTimerCallback, & context); CFRunLoopAddTimer(runLoop, timer, kCFRunLoopCommonModes);
?
配置Port-Based輸入源
Cocoa和Core Foundation都提供了port-based對象來進行線程間通訊。
?
配置NSMachPort對象
要建立和NSMachPort對象的聯系,需要創建一個port對象然后把它加到線程的run loop中。當啟動輔助線程時,把同樣的對象傳遞給線程的入口方法。輔助線程就可以使用相同的對象來把消息發送回來。
?
主線程代碼實現
下面展示了啟動輔助線程的代碼。Cocoa框架處理了port和run loop的一些中間步驟,所有啟動線程的方法比Core Fouundation要短。但是兩種效果是一樣的。不同的是這個方法直接發送NSPort對象給工作線程
- ( void )launchThread { NSPort * myPort = [NSMachPort port]; if (myPort) { // This class handles incoming port messages. [myPort setDelegate:self]; // Install the port as an input source on the current run loop. [[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode]; // Detach the thread. Let the worker release the port. [NSThread detachNewThreadSelector:@selector(LaunchThreadWithPort:) toTarget:[MyWorkerClass class ] withObject:myPort]; } }
如果要設置線程的雙向通訊,也需要工作線程把它的port發送給主線程。
#define kCheckinMessage 100 // Handle responses from the worker thread. - ( void )handlePortMessage:(NSPortMessage * )portMessage { unsigned int message = [portMessage msgid]; NSPort * distantPort = nil; if (message == kCheckinMessage) { // Get the worker thread’s communications port. distantPort = [portMessage sendPort]; // Retain and save the worker port for later use. [self storeDistantPort:distantPort]; } else { // Handle other messages. } }
輔助線程代碼實現
對于輔助線程,需要配置線程來使用port和主線程通訊。
下面講述了設置輔助線程。在創建autorelease pool之后,它創建了一個工作對象來控制線程執行。工作對象的sendCheckinMessage:方法給工作線程創建一個本地port然后發送消息給主線程。
+( void )LaunchThreadWithPort:( id )inData { NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; // Set up the connection between this thread and the main thread. NSPort* distantPort = (NSPort* )inData; MyWorkerClass * workerObj = [[self alloc] init]; [workerObj sendCheckinMessage:distantPort]; [distantPort release]; // Let the run loop process things. do { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; } while (! [workerObj shouldExit]); [workerObj release]; [pool release]; }
在使用NSMachPort時,線程間單向通訊可以使用同一個對象。也就是說,當前線程創建的port對象是其他線程接收到的port對象。
下面是輔助線程的check-in流程。這個方法設置了一個port用來在以后進行通訊,然后把它發送給主線程。這個方法使用LaunchThreadWithPort:方法傳過來的port對象
// Worker thread check-in method - ( void )sendCheckinMessage:(NSPort* )outPort { // Retain and save the remote port for future use. [self setRemotePort:outPort]; // Create and configure the worker thread port. NSPort* myPort = [NSMachPort port]; [myPort setDelegate:self]; [[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode]; // Create the check-in message. NSPortMessage* messageObj = [[NSPortMessage alloc] initWithSendPort:outPort receivePort:myPort components:nil]; if (messageObj) { // Finish configuring the message and send it immediately. [messageObj setMsgId:setMsgid:kCheckinMessage]; [messageObj sendBeforeDate:[NSDate date]]; } }
配置NSMessagePort對象
與NSMessagePort對象建立連接并不是見到的在線程間傳遞port對象。遠程port纖細必須要有一個名字。Cocoa用一個特定的名字注冊port然后把它傳遞給遠程線程來進行通訊。下面展示了創建和注冊消息port的代碼
NSPort* localPort = [[NSMessagePort alloc] init]; // Configure the object and add it to the current run loop. [localPort setDelegate:self]; [[NSRunLoop currentRunLoop] addPort:localPort forMode:NSDefaultRunLoopMode]; // Register the port using a specific name. The name must be unique. NSString* localPortName = [NSString stringWithFormat: @" MyPortName " ]; [[NSMessagePortNameServer sharedInstance] registerPort:localPort name:localPortName];
使用Core Foundation配置port-based輸入源
這里展示如何使用Core Foundation在主線程和工作線程之間設置一個雙向通訊通道。
主線程調用下面的方法來啟動工作線程。里面做的第一件事是設置了一個CFMessagePortRef類型來監聽工作線程的消息。工作線程需要port的名字來建立連接, 所以名字會在工作線程的入口傳過去。名字必須是唯一的。
#define kThreadStackSize (8 *4096) OSStatus MySpawnThread() { // Create a local port for receiving responses. CFStringRef myPortName; CFMessagePortRef myPort; CFRunLoopSourceRef rlSource; CFMessagePortContext context = { 0 , NULL, NULL, NULL, NULL}; Boolean shouldFreeInfo; // Create a string with the port name. myPortName = CFStringCreateWithFormat(NULL, NULL, CFSTR( " com.myapp.MainThread " )); // Create the port. myPort = CFMessagePortCreateLocal(NULL, myPortName, & MainThreadResponseHandler, & context, & shouldFreeInfo); if (myPort != NULL) { // The port was successfully created. // Now create a run loop source for it. rlSource = CFMessagePortCreateRunLoopSource(NULL, myPort, 0 ); if (rlSource) { // Add the source to the current run loop. CFRunLoopAddSource(CFRunLoopGetCurrent(), rlSource, kCFRunLoopDefaultMode); // Once installed, these can be freed. CFRelease(myPort); CFRelease(rlSource); } } // Create the thread and continue processing. MPTaskID taskID; return (MPCreateTask(& ServerThreadEntryPoint, ( void * )myPortName, kThreadStackSize, NULL, NULL, NULL, 0 , & taskID)); }
線程啟動之后,主線程在等待反饋時會繼續執行其他的任務。當反饋消息回來時,會分發到下面的MainThreadResponseHandler方法。
#define kCheckinMessage 100 // Main thread port message handler CFDataRef MainThreadResponseHandler(CFMessagePortRef local, SInt32 msgid, CFDataRef data, void * info) { if (msgid == kCheckinMessage) { CFMessagePortRef messagePort; CFStringRef threadPortName; CFIndex bufferLength = CFDataGetLength(data); UInt8 * buffer = CFAllocatorAllocate(NULL, bufferLength, 0 ); CFDataGetBytes(data, CFRangeMake( 0 , bufferLength), buffer); threadPortName = CFStringCreateWithBytes (NULL, buffer, bufferLength, kCFStringEncodingASCII, FALSE); // You must obtain a remote message port by name. messagePort = CFMessagePortCreateRemote(NULL, (CFStringRef)threadPortName); if (messagePort) { // Retain and save the thread’s comm port for future reference. AddPortToListOfActiveThreads(messagePort); // Since the port is retained by the previous function, release // it here. CFRelease(messagePort); } // Clean up. CFRelease(threadPortName); CFAllocatorDeallocate(NULL, buffer); } else { // Process other messages. } return NULL; }
主線程配置好之后,剩下的工作是給工作線程創建port。下面展示了工作線程的入口。
OSStatus ServerThreadEntryPoint( void * param) { // Create the remote port to the main thread. CFMessagePortRef mainThreadPort; CFStringRef portName = (CFStringRef)param; mainThreadPort = CFMessagePortCreateRemote(NULL, portName); // Free the string that was passed in param. CFRelease(portName); // Create a port for the worker thread. CFStringRef myPortName = CFStringCreateWithFormat(NULL, NULL, CFSTR( " com.MyApp.Thread-%d " ), MPCurrentTaskID()); // Store the port in this thread’s context info for later reference. CFMessagePortContext context = { 0 , mainThreadPort, NULL, NULL, NULL}; Boolean shouldFreeInfo; Boolean shouldAbort = TRUE; CFMessagePortRef myPort = CFMessagePortCreateLocal(NULL, myPortName, & ProcessClientRequest, & context, & shouldFreeInfo); if (shouldFreeInfo) { // Couldn't create a local port, so kill the thread. MPExit( 0 ); } CFRunLoopSourceRef rlSource = CFMessagePortCreateRunLoopSource(NULL, myPort, 0 ); if (! rlSource) { // Couldn't create a local port, so kill the thread. MPExit( 0 ); } // Add the source to the current run loop. CFRunLoopAddSource(CFRunLoopGetCurrent(), rlSource, kCFRunLoopDefaultMode); // Once installed, these can be freed. CFRelease(myPort); CFRelease(rlSource); // Package up the port name and send the check-in message. CFDataRef returnData = nil; CFDataRef outData; CFIndex stringLength = CFStringGetLength(myPortName); UInt8 * buffer = CFAllocatorAllocate(NULL, stringLength, 0 ); CFStringGetBytes(myPortName, CFRangeMake( 0 ,stringLength), kCFStringEncodingASCII, 0 , FALSE, buffer, stringLength, NULL); outData = CFDataCreate(NULL, buffer, stringLength); CFMessagePortSendRequest(mainThreadPort, kCheckinMessage, outData, 0.1 , 0.0 , NULL, NULL); // Clean up thread data structures. CFRelease(outData); CFAllocatorDeallocate(NULL, buffer); // Enter the run loop. CFRunLoopRun(); }
進入run loop后,其他發送過來的時間由ProcessClientRequest方法處理。這個方法怎么實現取決于這個線程想做什么。
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
