Run Loops
運行循環是與線程相關聯的基礎架構的一部分。runloop是一個事件處理循環,你可以使用它來處理你安排的工作和協調處理傳入的事件。runloop的目的就是當有任務的時候一直保持線程處理繁忙狀態,當沒有任務的時候讓線程處于休眠狀態。用來節約系統資源。
Cocoa和Core Foundation都提供runloop,以幫助您配置和管理線程的運行循環。您的應用程序不需要明確創建這些對象;每個線程,包括應用程序的主線程,都有一個關聯的運行循環對象。但是,只有在子線程需要顯式運行它們的運行循環。應用程序框架在應用程序啟動過程的一部分,自動設置和運行主線程上的運行循環。
Run Loop 解析
運行循環從兩種不同類型的源接收事件,輸入源提供異步事件,通常來自另一個線程或不同應用程序的消息。定時器源提供同步事件,發生在預定時間或重復間隔。這兩種類型的源使用應用程序在特定的情況下處理事件。
圖3-1顯示了運行循環和各種源的概念結構。輸入源將異步事件傳遞給相應的處理程序,并導致runUntilDate:方法在一定時間內(在線程的關聯NSRunLoop對象上調用)退出。定時器源將事件傳遞給其處理程序例程,但不會導致運行循環退出。
除了處理輸入源之外,運行循環還會生成關于運行循環行為的通知。注冊的運行循環觀察器(observers)可以接收這些通知,并使用它們對線程執行其他處理。您使用Core Foundation在您的線程上安裝run-loop觀察器。
以下部分提供有關運行循環的組件及其操作模式的更多信息。他們還描述了在處理事件時在不同時間生成的通知。
Run Loop 模式
NSDefaultRunLoopMode(Cocoa)
kCFRunLoopDefaultMode(Core Foundation)
默認模式是用于大多數操作的模式。大多數情況下,您應該使用此模式啟動運行循環并配置輸入源。
NSConnectionReplyMode(Cocoa)
Cocoa使用此模式結合NSConnection對象來監視回復。你很少需要自己使用這種模式。
NSModalPanelRunLoopMode(Cocoa)
Cocoa使用此模式來識別用于模態面板的事件。
NSEventTrackingRunLoopMode(Cocoa)
Cocoa使用此模式來限制除了在鼠標拖動和用戶界面跟蹤循環中的其他類型的傳入事件。
NSRunLoopCommonModes(Cocoa)
kCFRunLoopCommonModes(Core Foundation)
這是一組可配置的通用模式。將輸入源與此模式相關聯還將其與組中的每種模式相關聯。對于Cocoa應用程序,默認情況下,此設置包括默認模式和事件跟蹤模式。Core Foundation最初只包含默認模式。您可以使用該CFRunLoopAddCommonMode功能將自定義模式添加到集合中。
Input Sources(輸入源)
輸入源與您的線程異步傳遞事件。事件的來源取決于輸入源的類型,這通常是兩個類別之一。基于端口的輸入源監視應用程序的Mach端口。自定義輸入源監視自定義的事件源。就您的運行循環而言,輸入源是基于端口還是自定義都不重要。系統通常實現兩種類型的輸入源,您可以使用它們。兩個來源之間的唯一區別是如何發出信號。基于端口的源由內核自動發出信號,自定義源必須從另一個線程手動發出信號。
創建輸入源時,將其分配給運行循環的一個或多個模式。模式會影響在任何給定時刻監控哪些輸入源。大多數情況下,您在默認模式下運行運行循環,但也可以指定自定義模式。如果輸入源不在當前監視的模式,則其生成的任何事件將保持,直到運行循環以正確的模式運行。
以下部分介紹一些輸入源。
?基于端口的源
Cocoa和Core Foundation為使用端口相關對象和功能創建基于端口的輸入源提供內置支持。例如,在Cocoa中,您根本不必直接創建輸入源。您只需創建一個端口對象,并使用將該NSPort端口添加到運行循環的方法。端口對象為您處理所需的輸入源的創建和配置。
在Core Foundation中,您必須手動創建端口及其運行循環源。在這兩種情況下,您使用的端口類型不透明(相關的功能CFMachPortRef,CFMessagePortRef或CFSocketRef)創建合適的對象。
有關如何設置和配置自定義基于端口的源的示例,請參閱配置基于端口的輸入源。
自定義輸入源
要創建自定義輸入源,必須CFRunLoopSourceRef在Core Foundation中使用與不透明類型相關聯的函數。您可以使用多個回調函數配置自定義輸入源。Core Foundation在不同點調用這些函數來配置源,處理任何傳入的事件,并在從運行循環中刪除源時將其拆下。
除了在事件到達時定義自定義源的行為之外,還必須定義事件傳遞機制。源的這一部分運行在一個單獨的線程上,負責向輸入源提供其數據,并在該數據準備好進行處理時發出信號。事件傳遞機制取決于您,但不必過于復雜。
有關如何創建自定義輸入源的示例,請參閱定義自定義輸入源。有關自定義輸入源的參考信息,請參見“CFRunLoopSource參考”。
Cocoa Perform Selector 源
除了基于端口的源之外,Cocoa還定義了一個自定義輸入源,允許您在任何線程上執行選擇器。像基于端口的源一樣,執行選擇器請求在目標線程上被序列化,減輕了在一個線程上運行多個方法可能發生的許多同步問題。與基于端口的源不同,執行選擇器源在執行其選擇器后從運行循環中刪除自身。
當在另一個線程上執行選擇器時,目標線程必須具有活動的運行循環。對于您創建的線程,這意味著等待直到您的代碼顯式啟動運行循環。因為主線程啟動自己的運行循環,所以一旦應用程序調用applicationDidFinishLaunching:應用程序委托的方法,就可以開始在該線程上發出調用。運行循環每次循環處理所有排隊的執行選擇器調用,而不是在每個循環迭代期間處理一個。
以下是在其他線程上執行選擇器:
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
在該線程的下一個運行循環周期中,在應用程序的主線程上執行指定的選擇器。這些方法使您可以選擇阻止當前線程,直到執行選擇器。
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
在具有NSThread對象的任何線程上執行指定的選擇器。這些方法使您可以選擇阻止當前線程,直到執行選擇器。
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
在下一個運行循環周期和可選的延遲周期之后,在當前線程上執行指定的選擇器。因為它等待直到下一個運行循環周期來執行選擇器,所以這些方法提供了當前執行代碼的自動微型延遲。多個排隊的選擇器按照排隊的順序逐個執行。
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:
允許您使用performSelector:withObject:afterDelay:或performSelector:withObject:afterDelay:inModes:方法取消發送到當前線程的消息。
定時器源
定時器源將來會在預設的時間內向線程同步傳送事件。計時器是線程通知自己做某事的一種方式。
雖然它生成基于時間的通知,但定時器不是實時機制。與輸入源一樣,定時器與運行循環的特定模式相關聯。如果定時器未處于運行循環當前正在監視的模式,則在運行運行循環的定時器支持的模式之一之前,它不會觸發。類似地,如果在運行循環處于執行處理程序例程的中間時定時器觸發,則定時器等待直到下一次通過運行循環來調用其處理程序例程。如果運行循環沒有運行,則定時器永遠不會觸發。
您可以配置計時器一次或重復生成事件。重復定時器根據預定的開始時間自動重新調度,而不是實際的開始時間。例如,如果定時器計劃在特定時間和之后的每5秒開始,則即使實際的開始時間延遲,預定的開始時間將始終落在原始的5秒時間間隔上。如果開始時間延遲太多,以至于它錯過了一個或多個預定的開始時間,那么定時器只會在錯過的時間段內被觸發一次。對于錯過的周期,計時器將重新安排下次計劃的開始時間。
有關配置定時器源的更多信息,請參閱配置定時器源。有關參考信息,請參閱NSTimer類參考或CFRunLoopTimer參考。
Run Loop Observers(觀察者)
與發生適當的異步或同步事件時發生的源相反,在運行循環本身的執行期間,運行循環觀察者在特殊位置觸發。您可以使用運行循環觀察器來準備您的線程來處理給定的事件,或者在進入休眠之前準備線程。您可以在運行循環中將運行循環觀察者與以下事件相關聯:
運行循環的入口。
當運行循環即將處理定時器時。
當運行循環即將處理輸入源時。
當運行循環即將去睡覺時。
當運行循環已經喚醒,但在它已經處理了喚醒它的事件之前。
退出運行循環。
您可以使用Core Foundation將運行循環觀察器添加到應用程序。要創建一個運行循環觀察器,您將創建一個CFRunLoopObserverRef不透明類型的新實例。該類型跟蹤您的自定義回調函數及其感興趣的活動。
類似于定時器,可以使用一次或多次運行循環觀察器。單次觀察器在觸發后將其從運行循環中移除,而重復的觀察器仍然被附加。您指定觀察者在創建它時是否運行一次或多次。
有關如何創建運行環觀察器的示例,請參閱配置運行循環。有關參考信息,請參閱CFRunLoopObserver參考。
Run Loop 事件執行順序
每次運行它時,您的線程的運行循環處理等待事件,并為任何附加的觀察者生成通知。這樣做的順序是非常具體的,如下所示:
1 通知觀察者已經進入了運行循環。
2 通知觀察者,即將處理任何已經準備好的時間器源。
3 通知觀察員,即將處理任何不基于端口的輸入源。
4 處理非基于端口的輸入源。
5 如果基于端口的輸入源準備就緒并等待觸發,則立即處理該事件。轉到步驟9。
6 通知觀察者線程即將休眠。
7 讓線程休眠,直到發生以下事件之一:
? ?基于端口的輸入源事件即將到達。
? ?定時器觸發
? ?為運行循環設置的超時值過期。
? ?運行循環被明確喚醒。
8 通知觀察者線程被喚醒。
9 處理掛起的事件。
? ?如果用戶定義的定時器觸發,則處理定時器事件并重新啟動循環。轉到步驟2。
? ?如果輸入源觸發,則傳遞事件。
? ?如果運行循環被明確喚醒但尚未超時,請重新啟動循環。轉到步驟2。
10 通知觀察者運行循環已經退出。
因為定時器和輸入源的觀察者通知在這些事件實際發生之前被傳遞,所以在通知的時間和實際事件的時間之間可能存在差距。如果這些事件之間的時機是非常重要的,您可以使用睡眠和睡眠喚醒通知來幫助您將實際事件之間的時間相關聯。
因為在運行運行循環時會傳遞定時器和其他周期性事件,所以繞過該循環會中斷這些事件的傳遞。當您通過輸入循環并重復地從應用程序請求事件來實現鼠標跟蹤例程時,就會發生此行為的典型示例。因為您的代碼直接抓取事件,而不是讓應用程序正常發送這些事件,否則激活的計時器將無法啟動,直到您的鼠標跟蹤程序退出并返回到應用程序的控制。
運行循環可以使用運行循環對象顯式喚醒。其他事件也可能導致運行循環被喚醒。例如,添加另一個非基于端口的輸入源喚醒運行循環,以便可以立即處理輸入源,而不是等到發生其他事件。
什么時候使用Run Loop
您唯一需要顯式運行運行循環的方法是為應用程序創建子線程。應用程序主線程的運行循環是基礎架構的關鍵。因此,應用程序框架提供了運行主應用程序循環并自動啟動該循環的代碼。所述run的方法UIApplication在IOS(或NSApplication在OS X)啟動應用程序的主循環的正常啟動序列的一部分。如果您使用Xcode模板項目來創建應用程序,則不應顯式地調用這些例程。
對于子線程,您需要確定是否需要運行循環,如果是,請自行配置并啟動它。在所有情況下,您不需要啟動線程的運行循環。例如,如果使用線程執行一些長時間運行和預定的任務,那么您可以避免啟動運行循環。運行循環適用于您希望與線程進行更多交互的情況。例如,如果您計劃執行以下任何操作,則需要啟動運行循環:
?使用端口或自定義輸入源與其他線程進行通信。
在線程上使用計時器。
performSelector在Cocoa應用程序中使用任何...方法。
保持線程執行定期任務。
如果您選擇使用運行循環,則配置和設置很簡單。與所有線程編程一樣,您應該有一個在適當情況下退出子線程的計劃。通過讓它退出而不是強制它終止,總是更好地結束一個線程。有關如何配置和退出運行循環的信息,請參見“運行循環對象”。
使用 Run Loop 對象
運行循環對象提供了將輸入源,計時器和運行循環觀察器添加到運行循環然后運行的主接口。每個線程都有一個與之相關聯的運行循環對象。在Cocoa中,此對象是NSRunLoop該類的一個實例。在底層應用程序中,它是一個指向CFRunLoopRef不透明類型的指針。
獲取運行循環對象
要獲取當前線程的運行循環,請使用以下之一:
在Cocoa應用程序中,使用NSRunLoop的類方法currentRunLoop來獲取一個NSRunLoop對象。
在Core Foundation中使用?CFRunLoopGetCurrent()?來獲取當前Run Loop。
雖然它們不是免費的橋接類型,但是在需要時可以CFRunLoopRef從NSRunLoop對象獲取不透明的類型。本NSRunLoop類定義了一個getCFRunLoop返回的方法CFRunLoopRef類型,你可以傳遞給Core Foundation的例程。因為兩個對象引用相同的運行循環,所以可以根據需要將NSRunLoop對象和CFRunLoopRef不透明類型的調用混合起來。
配置Run Loop
在子線程上運行運行循環之前,必須至少添加一個輸入源或定時器。如果運行循環沒有任何來源進行監視,則當您嘗試運行它時會立即退出。有關如何向運行循環添加源的示例,請參閱配置運行循環源。
除了安裝源之外,您還可以安裝運行循環觀察者并使用它們來檢測運行循環的不同執行階段。要安裝運行循環觀察者,您將創建一個CFRunLoopObserverRef不透明類型,并使用該CFRunLoopAddObserver函數將其添加到運行循環中。必須使用Core Foundation創建運行循環觀察者,即使對于Cocoa應用程序也是如此。
下面顯示了將運行循環觀察器附加到其運行循環的線程的主例程。該示例的目的是向您展示如何創建運行循環觀察器,因此該代碼只需設置一個運行循環觀察器來監視所有運行循環活動。在處理定時器請求時,基本處理程序例程(未顯示)僅記錄運行循環活動。
// 用來監聽的函數
static void myRunLoopObserver (CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"進入");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"即將處理Timer事件");
break;
case kCFRunLoopBeforeSources:
NSLog(@"即將處理Source事件");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"即將休眠");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"被喚醒");
break;
case kCFRunLoopExit:
NSLog(@"退出RunLoop");
break;
default:
break;}}
- (void)threadMain
{
// 獲取當前runloop
NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
// 創建一個觀察者并添加到runloop中
CFRunLoopObserverContext? context = {0, (__bridge void *)(self), NULL, NULL, NULL};
CFRunLoopObserverRef? ? observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities, YES, 0, &myRunLoopObserver, &context);
if (observer){
// 添加觀察者
CFRunLoopRef? ? cfLoop = [myRunLoop getCFRunLoop];
CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);}
// 創建timer scheduld已經自動添加到runloop中了
[NSTimer scheduledTimerWithTimeInterval:1 target:self
selector:@selector(doFireTimer:) userInfo:nil repeats:YES];
NSInteger? ? loopCount = 10;
do{
// 這里是主線程的runloop 在主線程中無法退出 ,在子線程中這里會退出
[myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
loopCount--;}
while (loopCount);}
-(void)doFireTimer:(id)timer
{
NSLog(@"doFireTimer");
}
配置長時間線程的運行循環時,最好至少添加一個輸入源來接收消息。雖然可以僅使用定時器連接進入運行循環,但一旦定時器觸發,通常會失效,這將導致運行循環退出。安裝重復的定時器可以使運行循環在更長的時間內運行,但會涉及定時啟動定時器來喚醒線程,這實際上是另一種形式的輪詢。相比之下,輸入源等待事件發生,保持線程睡著,直到它發生。
開啟Run Loop
啟動運行循環僅對應用程序中的輔助線程是必需的。運行循環必須至少有一個輸入源或定時器來監視。如果沒有,運行循環將立即退出。
有幾種啟動運行循環的方法,包括:
run ? ? ? ?無條件
runUntilDate: ? ? ? ?設定時限
runMode:beforeDate: ? ? ?在特定模式下?
無條件進入運行循環是最簡單的選擇,但也是最不可取的。無條件地運行您的運行循環將線程放入永久循環,這使您無需對運行循環本身的控制。您可以添加和刪除輸入源和計時器,但停止運行循環的唯一方法是將其刪除。也沒有辦法在自定義模式下運行運行循環。
相比無條件地運行運行循環,最好用runUntilDate運行循環。當您使用runUntilDate時,運行循環將運行,直到事件到達或分配的時間到期。如果一個事件到達,則將該事件分派到處理程序進行處理,然后運行循環退出。您的代碼可以重新啟動運行循環來處理下一個事件。如果分配的時間到期,您可以簡單地重新啟動運行循環,或者使用時間進行所需的工作。
除了runUntilDate之外,您還可以使用特定模式運行運行循環。特定模式和runUntilDate不是互斥的,并且可以在啟動運行循環時使用。特定模式限制將事件傳遞到運行循環的源的類型,并在運行循環模式中更詳細地描述。
列表3-2顯示了線程主入口例程的骨架版本。該示例的關鍵部分顯示了運行循環的基本結構。實質上,您將輸入源和定時器添加到運行循環中,然后重復調用其中一個例程來啟動運行循環。每次運行循環例程返回時,您都會檢查是否出現可能有可能退出線程的條件。該示例使用Core Foundation運行循環例程,以便它可以檢查返回結果并確定運行循環退出的原因。您也可以使用NSRunLoop類的方法以類似的方式運行運行循環,如果您使用Cocoa并且不需要檢查返回值
注:官網的例子只是寫了在主線程運行一個特定模式的Run Loop,下面我進行了一些修改,在子線程中執行。添加一個定時器,然后將定時器添加到runloop中,runloop運行模式是10秒鐘,所以后面打印之后runloop變會退出,定時器就不會執行了。
- (void)skeletonThreadMain
{
// 在子線程中執行
NSLog(@"%@",[NSThread currentThread]);
// 獲取當前runloop
CFRunLoopRef runLoop =CFRunLoopGetCurrent();
// 上下文
CFRunLoopTimerContext context = {0,NULL,NULL,NULL,NULL};
// 創建timer
CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault,1,3,0,0,
&myCFTimerCallback,&context);
// 添加到runloop中
CFRunLoopAddTimer(runLoop,timer,kCFRunLoopCommonModes);
BOOL done = NO;
do{
SInt32? ? result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);
if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished ||result ==kCFRunLoopRunTimedOut ))
done = YES;
NSLog(@"runloop 已經退出");}
while (!done);
}
// 使用Core Foundation創建和計劃定時器
void myCFTimerCallback(CFRunLoopTimerRef timer, void *info)
{
NSLog(@"myCFTimerCallback");
}
2017-09-08 16:49:22.637 runloop應用[75136:41331947]{number = 3, name = (null)}
2017-09-08 16:49:22.637 runloop應用[75136:41331947] myCFTimerCallback
2017-09-08 16:49:25.641 runloop應用[75136:41331947] myCFTimerCallback
2017-09-08 16:49:28.638 runloop應用[75136:41331947] myCFTimerCallback
2017-09-08 16:49:31.637 runloop應用[75136:41331947] myCFTimerCallback
2017-09-08 16:49:32.638 runloop應用[75136:41331947] runloop 已經退出
可以遞歸運行一個運行循環。換句話說,您可以從輸入源或定時器的處理程序例程中調用CFRunLoopRun,CFRunLoopRunInMode或任何NSRunLoop方法來啟動運行循環。當這樣做時,您可以使用任何要運行嵌套運行循環的模式,包括外部運行循環使用的模式。
退出Run Loop
在處理事件之前,有兩種方法使運行循環退出:
配置運行循環以超時值運行。(上面例子中已經寫出來了)
告訴運行循環停止。(下面例子中)
使用超時值當然是首選,如果您可以管理它。指定超時值可以使退出之前的運行循環完成所有正常處理,包括傳遞通知以運行循環觀察器。
使用該CFRunLoopStop函數顯式停止運行循環會產生類似于超時的結果。運行循環發出任何剩余的運行循環通知,然后退出。不同的是,您可以在無條件啟動的運行循環上使用此技術。
盡管刪除運行循環的輸入源和計時器也可能導致運行循環退出,但這不是停止運行循環的可靠方法。一些系統例程將輸入源添加到運行循環以處理所需的事件。因為您的代碼可能不知道這些輸入源,它將無法刪除它們,這將阻止運行循環退出。
下面例子展示CFRunLoopStop的使用
static int i=0;
// 使用Core Foundation創建和計劃定時器
void myCFTimerCallback(CFRunLoopTimerRef timer, void *info)
{
NSLog(@"myCFTimerCallback");
if (i==4){
CFRunLoopStop(CFRunLoopGetCurrent());
NSLog(@"runloop 退出");}
i++;
}
- (void)skeletonThreadMainstop
{
// 在子線程中執行
NSLog(@"%@",[NSThread currentThread]);
// 獲取當前runloop
CFRunLoopRef runLoop =CFRunLoopGetCurrent();
// 上下文
CFRunLoopTimerContext context = {0,NULL,NULL,NULL,NULL};
// 創建timer
CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault,1,3,0,0,
&myCFTimerCallback,&context);
// 添加到runloop中
CFRunLoopAddTimer(runLoop,timer,kCFRunLoopCommonModes);
CFRunLoopRun();
}
2017-09-08 17:21:21.991 runloop應用[75819:41527674]{number = 4, name = (null)}
2017-09-08 17:21:21.992 runloop應用[75819:41527674] myCFTimerCallback
2017-09-08 17:21:24.992 runloop應用[75819:41527674] myCFTimerCallback
2017-09-08 17:21:27.993 runloop應用[75819:41527674] myCFTimerCallback
2017-09-08 17:21:30.992 runloop應用[75819:41527674] myCFTimerCallback
2017-09-08 17:21:33.992 runloop應用[75819:41527674] myCFTimerCallback
2017-09-08 17:21:33.992 runloop應用[75819:41527674] runloop 退出
上面打印信息可以看出runloop使用stop函數退出了。
線程安全和Run Loop對象
線程安全性取決于您用來操作運行循環的API。Core Foundation中的功能通常是線程安全的,可以從任何線程調用。但是,如果您正在執行改變運行循環配置的操作,那么盡可能從擁有運行循環的線程執行此操作仍然是最佳做法。
CoCoaNSRunLoop類并不像其Core Foundation的一般線程一樣安全。如果您正在使用NSRunLoop該類來修改運行循環,那么您只能從擁有該運行循環的同一線程執行此操作。將輸入源或計時器添加到屬于不同線程的運行循環可能會導致代碼崩潰或出現意外的行為。