這是一篇對Run Loop開發文檔《Threading Program Guide:Run Loops》的翻譯,來源于蘋果開發文檔。
Run loops 是和線程相關的基礎部分。一個run loop是一個用來調度工作和協調接受的事件的?循環。一個run loop的目的是有任務的時候保持線程忙碌,沒有任務的時候線程休眠。
Runloop的管理并不是完全自動的,你必須編寫線程代碼在合適的時間點啟動runloop,并且響應接收的事件。Cocoa和Core框架都提供了runloop對象供開發者配置和管理線程的runloop。然而你的應用顯示不需要創建這些對象,app的框架在程序啟動的過程中已經自動設置并且運行了在主線程的runloop。
下面的章節提供了更多關于run loops和怎么在應用中配置run loops的信息,更多的關于runloop 對象的信息查看NSRunLoop Class Reference
和CFRunLoop Reference
Run Loop解析
一個run loop和它的名字聽起來非常相似,它是一個你的線程進入的?循環,并且用戶使用它運行事件處理程序來應答事件。 你的代碼控制實現runloop的真正的?循環部分。換句話說,你的代碼提供了for
或者while
用來驅動run loop。在你的?循環內,你使用一個run loop對象來啟動事件處理代碼----這些代碼能夠接收事件并且調用已安裝的事件處理程序。
runloop接收的事件來自兩個不同類型的源,input source負責分發異步事件,消息通常來自其他的線程或者一個不同的應用程序。***timer source ***分發同步事件,這些事件發生在計劃的時間點或者重復的時間間隔。兩種類型的事件源都用一個應用程序特定的程序處理方式來處理到來的事件。
圖標3-1展示了runloop和各種各樣的事件源的概念結構,輸入源異步地將事件發送給相應的處理程序,并且導致 runUntilDate:
方法被在特定線程相關的run loop調用使得runloop終止,定時器源會給把事件傳遞給處理程序,但是不會導致runloop的終止。
除了處理輸入源,run loops也會生成關于run loop的行為的通知,注冊run-loop 觀察者可以接收這些通知并且可以使用這些通知在線程上做額外的處理。你可以使用Core Foundation在線程上添加run loop觀察者。
下面的節提供了更多關于run loop組成和run loop處理模式的信息,同樣描述了runloop在處理事件的不同時刻獲取到的通知。
1. Run Loop模式
一個run loop模式是一個將要被監聽額輸入源和定時器的集合,?以及等待run loop通知的觀察者集合。你每次啟動run loop,你顯示或者隱士的指定一個“模式”來運行,在run loop的運行過程中,只有和指定模式相關的源才會被監聽和分發它們的事件(相似的,只有和指定模式關聯的觀察者才能獲得run loop運行進度的通知),和其他模式相關的輸入源會將任何接收到的事件保存起來,直到后來以合適的模式?運行run loop。
在你的代碼中,你可以通過名字識別運行模式,Cocoa和Core Foundation都定義了一個默認的模式和其他幾個通用的模式,可以通過字符串在代碼中指定。你可以通過簡單為自定義的模式指定字符串名的方式實現自定義模式。雖然你在自定義模式下賦值的名字是任意的,但是這些模式的內容卻不是隨意的,你必須確保為你創建的模式添加一個或多個輸入源、定時器或者run loop觀察者,這樣自定義的模式才會可用。
你使用模式可以在特定run loop運行中過濾掉不想要的源的事件。大多數情況下,你會在“default”模式運行代碼。一個模態的面板,然而可能運行在“modal”模式下,因為在這?種模式下,只有和模態面板相關的源才能夠把事件傳遞到線程上。(這里是Mac開發的吧,不理解) ,對于次要的線程,你可以使用自定義的模式阻止低優先級的輸入源在時間要求比較嚴格的操作期間傳遞事件。
注意:模式和事件的輸入源要區別對待,模式不是事件的類型。比如:你不能使用模式去單獨匹配鼠標按下事件或者單獨匹配鍵盤事件。你可以使用模式來監聽一組不同的端口(ports),暫時掛起定時器。也可以改變正在被監控的源和run loop的觀察者。
表3-1列舉了Cocoa和Core Foundationd的標準模式和使用的描述信息,name這一欄列舉了在代碼中指定模式所使用的常量。
Default:大多數操作都使用的模式,大多數情況下你應該在這個模式下開啟run loop,配置輸入源。
Connection:Cocoa使用這個模式結合NSConnection對象來檢查依賴。你自己幾乎不會用到這種模式
Modal:Cocoa使用這個模式區分發送到模態面板的事件。
Event tracking:Cocoa用這個模式在鼠標拖拽和其他類型用戶界面操作跟蹤過程中限制輸入的事件。
Common modes:這是一個可以通常使用的模式的課配置的組合,和這個模式相關的輸入源同樣會和組里面的任意一個模式關聯。對于Cocoa application,這個組默認包含了default、modal、event tracking模式,Core Foundation初始化時僅僅包含了default模式,你可以使用CFRunLoopAddCommonMode 添加自定義的模式。
2輸入源
輸入源異步的向你的線程分發事件,事件的來源取決于輸入源的類型,通常是兩種類型的一種,基于端口的輸入源監控你的應用程序的Mach端口,自定義的輸入源監控自定義事件源。就你的run loop而言,它不會關心一個輸入源是自定義還是基于端口的。系統通常會實現兩種輸入源,你只管使用就可以了。兩種輸入源的唯一區別是他們的信號是怎么獲得的。基于端口的源?由內核發送信號,自定義的源必須手動的在其他線程發信號。
當你創建了一個輸入源,你給它指定一種或者多種運行模式,模式決定了那些輸入源在任意給定的時刻會被監視。大多數時間你在default模式下運行,但是也可以指定自定義的模式。如果一個輸入源并不在當前模式的監視范圍,它產生的任意事件都會被保存直到run loop運行在正確的模式。
2.1基于端口的源
Cocoa和Core Foundation為使用端口相關對象和功能創建基于端口的輸入源提供內置支持,比如在Cocoa里面,你從來不需要直接創建輸入源,你只需要創建一個端口對象,調用NSPort的方法在run loop上添加端口,端口對象處理需要的輸入源的創建和配置。
在Core Foundation,你必須手動的創建端口和run loop輸入源。在創建端口和輸入源的情況下,需要使用和對外不透透明的(開發文檔沒有描述)的類型(CFMachPortRef, CFMessagePortRef, or CFSocketRef)相關的函數創建合適的對象。
比如怎么創建一個和配置一個定制的基于端口的輸入源,參考 7.7 配置基于端口的輸入源
2.2 自定義輸入源
創建一個定制的輸入源,必須使用在Core Foundation中不透明類CFRunLoopSourceRef相關的函數,配置定制的輸入源用到幾個回調函數。Core Foundation會在不同的點調用這些函數配置源、處理到來的事件、在源從run loop移除的時候銷毀源。
除了定義自定輸入源在事件到來時的行為,你必須也定義事件的傳遞機制,輸入源的這部分運行在一個單獨的線程上,并且負責提供輸入源的數據、在數據準備處理的時候發信號給輸入源。事件的傳遞機制取決于你,但是不需要過于復雜。
有關如何創建自定義輸入源的示例,請7.1 定義一個自定義的輸入源
。有關自定義輸入源的參考信息,請參閱“CFRunLoopSource”。
2.3 Cocoa執行消息選擇器源--(Cocoa Perform Selector Sources)
除了基于端口的輸入源,Cocoa定義了一個自定義的輸入源允許你在任意線程上執行selector的,就像基于端口的輸入源,在目標線程上執行selector的請求被序列化了,減少了許多在多個方法同時執行在一個線程的情況下發生的同步問題。和基于端口不同的是,一個perform selector輸入源在執行完selector后會自動把自己從run loop移除。
在10.5 之前的OS X上,perform selector 輸入源主要給主線程發信息,在OS X10.5之后,可以給任意線程發消息。
當在線程上執行一個selector的時候,該線程必須有一個活躍的run loop,對于你創建的線程,這意味著一直等待到你的代碼顯示的開啟run loop。因為主線程已經開啟它的run loop了,所以程序一調用applicationDidFinishLaunching:
就向該線程發出調用,run loop每進行一次循環就會處理隊列化的perform selector的調用,而不是每次run loop循環處理隊列中的選一個處理。
表3-2列舉了定義在NSObject可以在其他線程上執行selecors的方法,因為這些方法定義在NSObject類里面,你可以在任何你可以訪問到Objective-C對象的線程中使用,包括POSIX線程。這些方法實際上并不創建新的線程去執行selector。
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
執行特定的selector在主線程的下一個run loop回路。這兩個方法給你提供了選項來阻斷當前線程直到selector被執行完畢。
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
執行特定的selector在任意線程上,這些線程通過NSThread對象表示。同樣提供了阻斷當前線程直到selector被執行。
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
在當前線程上下一個run loop回路中執行selector,并附加了延遲選項。因為它等待下一個run loop回路到來才執行selector,這些方法從當前執行的代碼中提供了一個自動的微小延遲。多個排隊的selector會按照順序一個一個的執行。
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:
讓你取消一個通過performSelector:withObject:afterDelay: or performSelector:withObject:afterDelay:inModes: method
方法發送到當前線程的消息。
每個方法更多詳細信息見
NSObject Class Reference.
2.4 定時器源
定時器源在一個未來預先設置的時間同步?地傳遞事件給你的線程,定時器也是一種線程通知自己做某些事情的實現方式。比如一個搜索框可以使用一個定時器去初始化一個自動搜索,在用戶用戶連續輸入關鍵字的時間間隔大于某個數時觸發搜索。延時的使用給了用戶一個在搜索開始之前盡可能多的去打印期望的關鍵字的機會。
雖然定時器產生了基于時間的通知,但是一個定時器并不是真正?實時機制。就像輸入源一樣,定時器關聯了你的run loop里的特定的模式。如果一個timer并不是處于run loop當前監控的模式,定時器在你以定時器支持的模式運行run loop之前就不會啟動。
相似的,一個定時器如果在run loop執行處理代碼的過程中開啟了,定時器會等到下一次run loop調用它的處理程序。如果run loop沒有運行,定時器永遠不會啟動。
你可以配置定時器一次或者重復的產生事件,一個重復的定時器自動的在一個預定的啟動(fire)時間開始重復調度自己,并不是從真正的定時器fire的時間開始算。比如,一個定時器被設定在特定的時間點啟動而且從那以后5秒鐘一次。預定的?fire時間將永遠會落?在于原來5s的時間間隔,如果真正的啟動時間延遲。如果啟動的時間延遲非常多以至于定時器錯過了一次或多次預定的fire時間點,定時器只會在錯過的時間片段內啟動一次,在錯過的時間段?fire后,定時器會重新設定下次預設的?fire時間。
配置定時器更多參考
7.6 配置定時器
,NSTimer Class Reference or CFRunLoopTimer Reference.
3. run loop 觀察者
與輸入源相反,當一個合適的同步或者異步事件發生時輸入源會fire.而run loop觀察者在run loop本身自己執行的過程中會在一個特殊的地方fire。你可以用run loop觀察者讓你的線程去處理一個給定的事件或者為run loop將要進入睡眠準備線程。你同樣可以將run loop觀察者和run loop下面的事件關聯起來。
- run loop的入口
- run loop將要處理一個定時器
- run loop 將要處理一個輸入源
- run loop 將要進入睡眠
- run loop 已經喚醒,但是還沒有處理喚醒run loop的事件
- 退出run loop
你可以給app用 Core Foundation 添加run loop觀察者,創建一個run loop觀察者,你創建了一個CFRunLoopObserverRef的類型的對象,這個類型?持續跟蹤你自定義的回調和它關心的run loop活動部分。
和定時器相似,run loop觀察者可以重復或者單次使用,一個單次使用的觀察者會在它fire后在run loop中移除,一個重復的觀察者依然依附在run loop上。單次還是重復可以在創建的時候指定。
有關如何創建run loop 觀察者的示例,請參閱
6.2 配置run loop
。有關參考信息,請參閱CFRunLoopObserver。
4. run loop一些列的事件
每次你運行run loop,你的線程的run loop會處理掛起的事件,并且會給它的觀察者發送通知。處理的順序是非常特別的,就是下面順序。
1.通知觀察者run loop已經進入了循環。
2.通知觀察者所有準備就緒的定時器將要 fire
3.通知觀察者所有非基于端口的輸入源將要 fire
4.fire所有非基于端口的準備fire的輸入源
5.如果一個基于端口的輸入源準備好了并且等待fire。立刻fire。到第9部。
6.通知觀察者線程將要睡眠
-
7.將線程睡眠直到下面任意一個事件發生。
- 一個事件到達了基于端口的源
- 定時器fire
- run loop設置了到期的超時事件
- 顯示的指定run loop喚醒
8.通知觀察者線程已經喚醒。
-
9.處理掛起的事件。
- 如果一個用戶定義的定時器fire。處理定時器事件并且重新啟動run loop。到步驟2.
- 如果一個輸入源fire,傳遞事件。
- 如果run loop是被顯示的被喚醒,但超時事件還沒有到,重新啟動run loop進入步驟2.
10.通知觀察者run loop已經退出。
因為觀察者從定時器和輸入源來的通知會在那些事件實際發生之前被傳遞過來,可能在事件發生的時刻和收到通知的時刻之間有間隔,如果在事件上時效性是非常嚴格的,你可以使用睡眠和從睡眠中醒來的通知來幫助你關聯事件之間的時間。
因為定時器和其他的周期性的事件會在你運行run loop的時候傳遞,所以要避免run loop對事件傳遞的打斷。一個經典行為:每當你通過一個循環不斷的從應用程序請求事件來實現一個鼠標的跟蹤程序的時候。因為你的代碼是直接捕獲的事件,而不是讓應用程序正常的分發這些事件,活躍的定時器將在你的鼠標跟蹤程序退出并將控制權返回給應用程序后失效。
一個run loop可以用run loop對象顯示的喚醒,其他的事件同樣可以使run loop喚醒。比如添加其他的非基于端口的輸入源可以喚醒run loop可以使得輸入源可以立即被處理,而不是等到其他事件發生的時候。
5.什么時候會用一個run loop
唯一需要顯示的運行一個run loop的場景是在應用程序中創建了輔助線程。應用程序主線程的run loop是基礎設施的關鍵部分。所以app的框架提供了運行主線程run loop的代碼并且自動開啟。iOS的UIAppliaction的run方法(或者OS X 的NSApplication)開啟一個應用程序的main loop作為一些列程序啟動流程的一部分。如果你使用xcode模板工程創建應用,你應該從來不顯示的調用這些例程。
對于輔助線程,你需要決定一個run loop是不是必要的,如果是,就配置并開啟它。你并不需要在任意情況下都開啟一個線程的run loop。比如:如果你使用一個線程執行某些長時間運行并且是事先確定的任務,你可以避免開啟run loop。run loops的目的是為了應用在你想和線程有更多的交互的場合上的。比如:如果你想做下面的任何事情你就需要開啟run loop。
- 使用端口或者自定義的輸入源和其他線程通信
- 在線程上使用定時器
- 在cocoa應用中使用任意一個
performSelector…
方法 - 使得線程不被殺死去做周期性任務
如果你選擇使用一個run loop,配置和創建是非常簡單的。和所有的線程編程一樣,你為在合適的場合下結束你的輔助線程指定計劃。通常來說讓線程以結束的退出(exit)的方式要比強制讓線程終止的辦法好。怎么配置和退出run loop的描述信息在** 6. 使用run loop對象**.
6. 使用run loop對象
一個run loop對象提供了添加輸入源,定時器,觀察者和運行run loop的主要接口,每一個線程都單獨有一個run loop對象和它關聯。在 Cocoa中這個對象是NSRunLoop類的一個實例,在低層次的應用中,是一個CFRunLoopRef類型的指針。
6.1 獲取一個run loop對象
獲取當前線程的run loop只需要用下面的一種方法:
- 在Cocoa應用,使用NSRunLoop類的類方法currentRunLoop返回一個NSRunLoop對象
- 使用CFRunLoopGetCurrent函數
雖然這兩個并不是可以自由的橋接類型,但是你在必要的時候可以從一個NSRunLoop對象中獲取一個CFRunLoop類型。 通過NSRunLoop的getCFRunLoop方法獲得,然后傳遞給Core Foundation的代碼。因為兩個對象引用了相同的run loop,你可以根據需要隨意調用。
6.2 配置run loop
當你在一個輔助線程上開啟run loop之前,必須給run loop添加至少一個輸入源或者一個定時器。如果一個run loop沒有任何源來監控,就會立刻退出。參考7.配置run loop源
除了添加輸入源,你可以添加run loop觀察者,并且使用他們監測run loop不同階段的操作,添加觀察者要創建一個 CFRunLoopObserverRef 類型的對象,用CFRunLoopAddObserve函數添加到run loop上。觀察者必須用Core Foundation創建,即使在Cocoa應用中。
3-1是一個綁定了觀察者的線程開啟它的run loop的代碼。這個案例主要展示怎么創建run loop觀察者,所以代碼只是簡單的創建了一個觀察者來監控run loop的所有的活動。基本的處理程序(沒有展示)簡單地在處理定時器請求的時候記錄了run loop的活動。
Listing 3-1 Creating a run loop observer
- (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,一旦定時器fire,就無效了。會導致run loop退出。綁定一個重復的定時器可以使得run loop在一個長的時間段運行。但是需要定期的觸發定時器喚醒你的線程。這實際上是另一種形式的輪詢。相反,一個輸入源會等待事件的發生,在次之前會保持線程的休眠。
6.3 開啟run loop
在應用中開啟run loop僅僅對于輔助線程是必要的,run loop必須有至少一個輸入源或者定時器去監控,如果一個都沒有就會立刻結束。
下面是幾種開啟run loop的方法:
- 無條件的(Unconditionally)
- 帶有時間限制設置的(With a set time limit)
- 在特定的模式下(In a particular mode)
無條件的進入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事件的輸入源的類型。(詳細信息1. Run Loop模式.
)
3-2 是一個線程的主要代碼結構,關鍵部分是這個案例展示了run loop的基本結構,實際上你可以給run loop添加自己的輸入源和定時器然后重復的從多個程序例程中調用一個來啟動run loop。每次run loop例程程序返回,你檢查看看是否有任何可能導致線程結束的條件出現了。這個例子用了Core Foundation run loop程序,所以它可以檢查返回結果并且知道為什么run loop退出了,如果你用Cocoa,同樣可以用 NSRunLoop的方法以一個相似的方式運行run loop而且不用檢查返回值,在3-14.
Listing 3-2 Running a 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,或者其他的任意的NSRunLoop方法。當這樣做的時候,你可以使用任何你想要的模式運行嵌套的run loop,包括外層的run loop使用的模式。
6.4 退出 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增加輸入源處理必要的事件。因為你代碼可能沒有意識到這些輸入源的存在,它不能移除掉這些輸入源,這會阻止run loop的退出。
7. 線程安全和Run Loop對象
線程安全的差異取決于你操作run loop所使用的API,在Core Foundation的函數通常是線程安全的,而且可以被任何線程調用。然而如果你在執行run loop配置的操作,盡可能的從該run loop對應的線程上操作依然是一個好的做法。
Cocoa的NSRunLoop類并不是像在Core Foundation中那樣線程安全的,如果你使用NSRunLoop來修改你的run loop,你應該僅僅在run loop對應的那個線程上操作。添加一個輸入源或者定時器給非當前線程的run loop會導致你的代碼崩潰或者產生不可預測的行為。
7. 配置 Run Loop 資源
下面章節的代碼是一些如何設置不同類型輸入源的案例(Cocoa和Foundation)
7.1 定義一個自定義的輸入源
創建一個自定義的輸入源包含如下定義選項
- 輸入源希望處理的信息
- 一個調度程序讓感興趣的客戶(client)知道怎么和你的輸入源取得聯系
- 一個處理程序負責執行客戶(client)發來的請求
- 一個取消程序讓你輸入源無效
因為你自己創建一個自定義的輸入源來處理自定義的信息,實際的配置的設計是靈活的。調度程序和取消程序是關鍵程序,你的自定義輸入源幾乎總是需要的,剩下的大部分輸入源行為發生在這些程序之外。比如你可以定義傳遞數據給你的輸入源的機制和將輸入源的存在傳遞給其他線程。
圖3-2是一個自定義輸入源配置的案例。這案例中程序的主線程維護對輸入源、自定義輸入源的自定義命令緩沖區、輸入源所在的run loop的引用。當主線程有一個任務要交給工作線程的時候,它會向命令緩沖區發送一個命令和工作線程需要的所有開始任務所需要的信息。(因為主線程和工作線程都有訪問命令緩沖區的權限,訪問必須是同步的)一旦受到喚醒的命令,run loop調用輸入源的處理程序來處理在命令緩沖區的命令。
下面的章節解釋了上面圖標自定義輸入源的實現,和關鍵要實現的代碼
7.2 定義輸入源
自定義一個輸入源需要用Core Foundation的代碼來配置run loop資源,并且將它和run loop依附在一起。雖然基礎的處理程序是C函數,但是并不排除你需要用OC或者C++來封裝這些函數來實現你的代碼主體。
圖3-2中介紹的輸入源使用了OC對象來管理一個命令行緩沖區,協調run loop。3-3展示的是這個對象的定義,RunLoopSource對象管理一個命令行緩沖區,用緩沖區接收其他線程的消息。3-3同樣展示了RunLoopContext對象的定義,這是一個真正的用來傳遞一個RunLoopSource對象和run loop的引用到應用程序主線程的容器對象。
Listing 3-3 The custom input source object definition
@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
雖然輸入源的自定義的數據是OC代碼管理的,但是將輸入源和run loop關聯在一起的代碼需要基于C的回調函數,這些函數的第一個會在你真正將run loop源和run loop綁定的時候調用,在3-4,因為輸入源只有一個客戶(主線程)它使用調度程序中的函數發送一個信息來將自己在那個線程的應用代理上注冊自己。當代理想和輸入源取得聯系的時候,就會使用RunLoopContext對象來實現。
Listing 3-4 Scheduling a run loop source
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];
}
最重要的回調程序之一是用來在輸入源收到到信號時處理自定義數據的,3-5展示了執行和RunLoopSource對象相關的回調代碼。這個函數簡單的轉發了工作請求給sourceFired方法,這個方法會在以后處理命令緩沖區內出現的任何命令。
Listing 3-5 Performing work in the input source
void RunLoopSourcePerformRoutine (void *info)
{
RunLoopSource* obj = (RunLoopSource*)info;
[obj sourceFired];
}
如果你使用CFRunLoopSourceInvalidate函數將輸入源移除,系統會調用輸入源的取消代碼。你可以用這個代碼通知客戶們你的輸入源已經不再有效了,他們應該移除和它的所有的關聯。3-6是RunLoopSource對象注冊的取消回調代碼。這個函數發送另一個RunLoopContext對象給應用代理,但是這次是請求代理移除run loop源的關聯。
Listing 3-6 Invalidating an input source
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];
}
備注:應用程序代理的
registerSource: and removeSource:
方法在Coordinating with Clients of the Input Source
7.3 在run loop上添加輸入源
3-7展示了RunLoopSource的init和addToCurrentRunLoop方法。init方法創建了必須依附到RunLoop上的CFRunLoopSourceRef非透明類型對象,它通過傳遞RunLoopSource對象本身作為上下文信息,所以回調程序會有指向該對象的指針。輸入源的安裝工作不會在工作線程調用addToCurrentRunLoop方法前進行,addToCurrentRunLoop調用時RunLoopSourceScheduleRoutine的回調函數就會被調用,一旦輸入源添加到run loop,線程就可以運行它的run loop來等待事件。
Listing 3-7 Installing the run loop source
- (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);
}
7.4 協調輸入源的客戶
為了輸入輸入源起作用,你應該巧妙控制它并且在另一個線程給它發信號。輸入源的要點讓和它關聯的線程睡眠直到有事可做。所以讓其他的線程能夠獲得輸入源的信息和并且和輸入源進行通信是現實的需求。
一個通知輸入源的客戶的方式是當輸入源第一次安裝在run loop上的時候發送注冊請求。可以為一個輸入源注冊多個客戶,也可以簡單的注冊到一些中心機構,然后在把輸入源給感興趣的客戶。3-8展示了應用程序代理的注冊并在RunLoopSource對象的調度函數被調用時執行的注冊方法,這個方法接收RunLoopSource對象提供的RunLoopContext對象,并且把它添加到源列表上,下面的代碼也包含了在從run loop移除的時候如何反注冊輸入源。
Listing 3-8 Registering and removing an input source with the application delegate
- (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];
}
回調函數調用的方法在上面的3-4和3-6
7 .5 給輸入源發信號
當一個客戶把它的數據傳遞給輸入源后,必須給輸入源發信號喚醒它的run loop,給輸入源發信號讓run loop知道輸入源已經準備好,等待處理。因為一個信號發生的時候線程可能正在休眠,你應該總是顯示的喚醒run loop。如果不這樣做可能會導致處理輸入源的數據上產生延遲。
3-9展示了RunLoopSource 對象的fireCommandsOnRunLoop方法,客戶在他們為輸入源做好處理緩沖區數據的準備時調用這個方法。
Listing 3-9 Waking up the run loop
- (void)fireCommandsOnRunLoop:(CFRunLoopRef)runloop
{
CFRunLoopSourceSignal(runLoopSource);
CFRunLoopWakeUp(runloop);
}
備注:你不應該嘗試通過發送自定義輸入源來處理SIGHUP或其他類型的進程級信號,用于喚醒Run Loop的Core Foundation功能不是信號安全的,不應該在應用程序的信號處理程序中使用。 有關信號處理程序例程的更多信息,請參閱
sigaction
手冊頁。
7.6 配置定時器
要創建定時器源,你只需創建一個定時器對象并在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:方法將其添加到運行循環中,也可以手動調度計時器。這兩種技術基本上都是一樣的,但是給你不同級別的控制定時器配置。 例如,如果創建定時器并手動將其添加到運行循環中,則可以使用除默認模式之外的模式來執行此操作。 清單3-10顯示了如何使用這兩種技術創建定時器。 第一個定時器的初始延遲為1秒,但隨后每0.1秒鐘定時fire。 第二個定時器在初始0.2秒延遲后開始首次fire,然后每0.2秒fire一次。
Listing 3-10 Creating and scheduling timers using NSTimer
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];
清單3-11顯示了使用Core Foundation函數配置定時器所需的代碼。 雖然此示例不會在上下文結構中傳遞任何用戶定義的信息,但您可以使用此結構傳遞定時器所需的任何自定義數據。 有關此結構的內容的更多信息,請參閱CFRunLoopTimer
參考中的描述。
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);
7.7 配置基于端口的輸入源
Cocoa和Core Foundation都提供基于端口的對象,用于線程之間或進程之間的通信。 以下部分將介紹如何使用幾種不同類型的端口設置端口通信。
7.7.1 配置NSMachPort對象
要建立與NSMachPort對象的本地連接,你將創建端口對象并將其添加到主線程的Run Loop中。 啟動輔助線程時,將相同的對象傳遞給線程的入口點函數。 輔助線程可以使用相同的對象將消息發送回主線程。
7.7.2 實現主線程代碼
清單3-12顯示了啟動輔助工作線程的主線程代碼。 因為Cocoa框架執行了許多用于配置端口和run loop的介入步驟,所以launchThread方法明顯短于其Core Foundation中等效的配置(清單3-17);然而,兩者的行為幾乎相同。 一個區別是,該方法不是將本地端口的名稱發送給工作線程,而是直接發送NSPort對象。
Listing 3-12 Main thread launch method
- (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];
}
}
為了在線程之間建立一個雙向通信通道,你可能希望工作線程在登錄消息中將自己的本地端口發送到主線程。 接收簽入消息讓你的主線程知道在啟動第二個線程時一切順利,并且還可以向你發送更多消息到該線程。清單3-13顯示了主線程的handlePortMessage:方法。 當數據到達線程自己的本地端口時調用此方法。 當一個簽到消息到達時,該方法直接從端口消息中檢索次要線程的端口,并保存以備以后使用。
Listing 3-13 Handling Mach port messages
#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.
}
}
7.7.3實現次要線程代碼
對于輔助工作線程,你必須配置線程并使用指定的端口將信息傳回主線程。
清單3-14顯示了設置工作線程的代碼。 為線程創建自動釋放池后,該方法將創建一個工作對象來驅動線程執行。 工作對象的sendCheckinMessage:方法(如清單3-15所示)為工作線程創建一個本地端口,并將一個簽入消息發送回主線程。
Listing 3-14 Launching the worker thread using Mach ports
+(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時,本地和遠程線程可以使用相同的端口對象進行線程之間的單向通信。 換句話說,由一個線程創建的本地端口對象將成為另一個線程的遠程端口對象。
清單3-15顯示了次要線程的簽入例程。 該方法設置自己的本地端口用于將來的通信,然后發送一個檢入消息回主線程。 該方法使用在LaunchThreadWithPort:方法中接收的端口對象作為消息的目標。
Listing 3-15 Sending the check-in message using Mach ports
- (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]];
}
}
7.7.4 配置一個NSMessagePort對象
要建立與NSMessagePort對象的本地連接,您不能簡單地在線程之間傳遞端口對象。 遠程消息端口必須以名稱獲取。 在Cocoa中可能需要使用特定的名稱注冊本地端口,然后將該名稱傳遞給遠程線程,以便它可以獲取適當的端口對象進行通信。 清單3-16顯示了要使用消息端口的端口創建和注冊過程。
Listing 3-16 Registering a message 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];
7.7.6 Core Foundation中配置基于端口的輸入源
本節介紹如何使用Core Foundation在應用程序的主線程和工作線程之間設置雙向通信通道。清單3-17顯示了應用程序主線程調用的代碼,以啟動工作線程。 代碼的第一件事是設置一個CFMessagePortRef opaque類型來監聽來自工作線程的消息。 工作線程需要進行連接的端口名稱,以便將字符串值傳遞給工作線程的入口點函數。 端口名稱通常在當前用戶上下文中是唯一的; 否則,您可能會遇到沖突。
Listing 3-17 :將Core Foundation消息端口附加到新線程
#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函數,如清單3- 18。 此函數提取工作線程的端口名稱,并創建未來通信的管道。
Listing 3-18 Receiving the checkin message
#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;
}
在配置主線程之后,唯一剩下的就是新創建的工作線程創建自己的端口并簽入。清單3-19顯示了工作線程的入口點函數。 該函數提取主線程的端口名稱,并使用它來創建一個遠程連接回主線程。 該函數然后為其自身創建本地端口,將端口安裝在線程的運行循環上,并向包含本地端口名稱的主線程發送檢入消息。
Listing 3-19 Setting up the thread structures
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函數處理。 該功能的實現取決于線程工作的類型,此處未顯示。