什么是Run Loops
Run Loops是與線程想關(guān)聯(lián)的基礎(chǔ)部分。一個Run Loop就是事件處理循環(huán),它是用來調(diào)度和協(xié)調(diào)接收到的事件處理。使用Run Loop的目的双吆,就是使得線程有工作需要做時可以忙碌起來,而當(dāng)沒有事可做時会前,又可以使得線程睡眠好乐。
Run Loop管理不都是自動的。我們必須手動設(shè)計線程代碼瓦宜,在合適的時候來啟動Run Loop蔚万,并回應(yīng)到來的事件。Cocoa和Core Foundation都提供了run loop對象來幫助我們配置和管理線程的run loop临庇。我們的應(yīng)用沒有必要顯式地創(chuàng)建這些對象反璃;每個線程,包括應(yīng)用程序的主線程假夺,都有一個與之關(guān)聯(lián)的run loop淮蜈。只有子線程才需要顯式地運行其run loop。App會將自動配置和來運行主線程的run loop的任務(wù)作為應(yīng)用程序啟動處理的一部分已卷。
對于想要更深入地了解Run Loop Objects梧田,閱讀NSRunLoop Class Reference、CFRunLoop Reference
一個Run Loop的結(jié)構(gòu)
Run Loop就像它的名字一樣,它使得線程進(jìn)入事件循環(huán)裁眯,能對到來的事件啟動事件處理鹉梨。你的代碼中提供了流程控制說一句來實現(xiàn)run loop實實在在的循環(huán)部分,換句話說穿稳,你的代碼提供了while或者for循環(huán)來驅(qū)動run loop存皂。在你的循環(huán)中,你使用run loop對象在事件到達(dá)時逢艘,運行事件處理的代碼并調(diào)起已安裝的處理程序旦袋。
Run Loop接收來自兩種不同類型的源(sources)的事件:
輸入源:異步傳遞事件,通常是來自不同的線程或不同的應(yīng)用的消息它改。輸入源異步傳遞事件到對應(yīng)的處理程序和在線程關(guān)聯(lián)的NSRunLoop對象調(diào)起runUntilDate:方法來退出事件處理猜憎。
Timer源:同步地傳遞事件,發(fā)生在每個定時器調(diào)用或周期性地調(diào)用搔课。Timer源傳遞事件到他們的處理程序,但是不會調(diào)用run loop來退出處理截亦。
這兩種源在事件到達(dá)時都使用應(yīng)用程序特定的處理程序來處理事件爬泥。
如下圖所示,展示了run loop和不同的源的概述結(jié)構(gòu)崩瓤。
除了處理輸入源之外袍啡,run loops還發(fā)出關(guān)于run loop行為的通知。我們可以注冊成為run loop的觀察者却桶,就可以接收這些通知和使用它在線程上做一些額外處理境输。我們可以使用Core Foundation在對應(yīng)的線程上注冊成為run loop的觀察者。
Run Loop Modes
Run Loop模式是一個監(jiān)視輸入源和定時器的集合和注冊成為run loop的觀察者的集合颖系。每次要運行run loop嗅剖,都需要顯示或隱式地指定某種運行的mode。只有與這種指定的mode關(guān)聯(lián)的源才會被監(jiān)視和允許傳遞他們的事件嘁扼,同樣地信粮,只有與這種模式關(guān)聯(lián)的觀察者都會收到run loop行為變化的通知。與其它模式想關(guān)聯(lián)的源趁啸,直到隨后在合適的模式通過循環(huán)后强缘,都會接收到新的事件(比如,將timer加入run loop default模式下不傅,當(dāng)滾動時旅掂,timer不會收到回調(diào),直到停止?jié)L動回到default模式下)访娶。
在我們的代碼中商虐,我們通過名稱來唯一標(biāo)識mode。在Cocoa和Core Foundation中都定義了default模式和幾個常用的模式,都是通過字符串名稱來指定称龙。我們也可以自定義模式留拾,但是我們需要手動添加至少一個input source/timers/observers。
我們可以通過使用mode來過濾掉我們不希望接收到來自不想要的通過run loop的源鲫尊。大部分情況下痴柔,我們都是使用系統(tǒng)定義的default模式。對于子線程疫向,我們可以使用自定義模式在關(guān)鍵性操作時阻止低優(yōu)先級的源傳遞事件咳蔚。
注意:Modes是通過事件源來區(qū)分,而不是事件類型來區(qū)分搔驼。比如說谈火,我們不能使用mode來匹配只有mouse-down事件或者只有鍵盤事件。我們可以使用modes來監(jiān)聽不同系統(tǒng)的端口舌涨,臨時掛起定時器糯耍,甚至改變正在被監(jiān)視的sources和run loop觀察者。
Input Sources
輸入源異步傳遞事件到你的線程囊嘉。事件的源由輸入源的類型來決定温技,也就是兩種源中的其中一種:
Port-based:基于端口號的輸入源監(jiān)聽?wèi)?yīng)用程序的Mach端口。
Custom Input Sources:自定義輸入源監(jiān)聽自定義的事件源扭粱。
系統(tǒng)通常實現(xiàn)了這兩種輸入源舵鳞。唯一的不同點是它們是如何被發(fā)出信號的。port-based源是由內(nèi)核(kernel)自動發(fā)出信號琢蛤,而custom sources必須手動從其它線程發(fā)出信號蜓堕。
當(dāng)我們創(chuàng)建輸入源時,可以指定mode博其。Modes會影響任何時刻被監(jiān)視的輸入源套才。大部分情況下,我們都讓run loop在default mode下運行贺奠,但是也可以指定自定義的mode霜旧。如果一個輸入源不是當(dāng)前所監(jiān)視的model,它所產(chǎn)生的任何事件都會被保留直接進(jìn)入正常的mode儡率。
Port-Based Sources
Cocoa和Core Foundation提供了內(nèi)建支持挂据,可以使用與port相關(guān)的對象和函數(shù)來創(chuàng)建基于端口的輸入源。舉個例子儿普,在Cocoa中永遠(yuǎn)不需要手動創(chuàng)建輸入源崎逃。我們只需要簡單地創(chuàng)建一個port對象和使用NSPort的方法。port對象為我們處理所需要的輸入源的創(chuàng)建和配置眉孩。
在Core Foundation中个绍,我們必須手動創(chuàng)建port和source痴颊。在這兩種情況下币喧,我們可以使用與port opaque type關(guān)聯(lián)的函數(shù)(CFMessagePortRef, or CFSocketRef) 來創(chuàng)建合適的對象判没。
Custom Input Sources
在Core Foundation中啊送,要創(chuàng)建自定義輸入源,我們必須使用與CFRunLoopSourceRef關(guān)聯(lián)的函數(shù)广恢。我們配置自定義輸入源可以使用幾個回調(diào)函數(shù)凯旋。Core Foundation會在不同點回調(diào)這些函數(shù)來配置source,處理任何到達(dá)的事件和銷毀已從run loop移除的source钉迷。
除了定義在事件到達(dá)時自定義源的行為之外至非,我們也必須定義事件傳遞機制。這部分源運行在單獨的線程糠聪,負(fù)責(zé)提供輸入源的數(shù)據(jù)荒椭,當(dāng)數(shù)據(jù)準(zhǔn)備好可以處理時,signaling(通知相關(guān)線程)這個消息舰蟆。事件傳遞機制是我們自己來決定趣惠,但是不需要過于復(fù)雜。
Cocoa Perform Selector Source
除了基于端口的源之外身害,Cocoa還定義了自定義輸入源允許我們在任意線程上執(zhí)行selector信卡。就像port-based源一樣,執(zhí)行selector請求會在目標(biāo)線程上序列化题造,以減少在同一個線程中出現(xiàn)多個方法同步執(zhí)行的問題。與port-based源不同的是猾瘸,執(zhí)行selector源在執(zhí)行完畢后會自動將自己從run loop中移除界赔。
當(dāng)執(zhí)行在其它線程執(zhí)行selector時,目標(biāo)線程必須要有運行的run loop牵触。當(dāng)我們創(chuàng)建線程時淮悼,這意味著直到啟動了run loop都會顯式地執(zhí)行selector代碼。
Run Loop每次經(jīng)過一個循環(huán)揽思,就會處理隊列中所有的selector袜腥,而不僅僅是處理一個。
Timer Sources
Timer源在未來設(shè)定的時間會同步地傳遞事件到你的線程羹令。Timers是線程通知自己去做一些事情的一種方式。比如說损痰,搜索框可以使用定時器來初始化在一定時間就自動搜索福侈,以便提供更多地聯(lián)想詞給用戶。
盡管它發(fā)送基于時間的通知卢未,但定時器并不是一種實時的機制肪凛。像輸入源一樣堰汉,定時器只有與run loop的mode一樣才會發(fā)送通知。如果timer在run loop中并不是所被監(jiān)視的mode伟墙,它不會觸發(fā)定時器翘鸭,直到run loop的mode與timer所支持的mode一樣。
同樣地戳葵,如果run loop正在處理中就乓,timer已經(jīng)fire了,這時候會被中斷譬淳,直到下一次通過run loop才會調(diào)志處理程序档址。如果run loop已經(jīng)不再運行了,則timer永遠(yuǎn)不會再fire邻梆。
我們可以配置timer只產(chǎn)生事件一次或者重復(fù)產(chǎn)生守伸。重復(fù)的timer會自動根據(jù)調(diào)度的firing time自動調(diào)度,而不是真實的firing time浦妄。比如說尼摹,如果一個timer在特定的時間調(diào)度,然后每5秒重復(fù)一次剂娄。如果firing time被延遲導(dǎo)致缺少一或多次調(diào)用蠢涝,那么timer在缺失的周期中只會調(diào)用一次。
Run Loop Observers
與sources在適當(dāng)時機異步或同步發(fā)出事件不同阅懦,observers在run loop本身執(zhí)行期間和二,會在特定的地方發(fā)出。你可能需要到run loop observers去準(zhǔn)備線程處理特定的事件或者在進(jìn)入睡眠之前耳胎。我們可以通過以下事件來關(guān)聯(lián)run loop observers:
進(jìn)入run loop
run loop將要處理timer
run loop將要處理輸入源
run loop將要進(jìn)入睡眠
run loop被喚醒惯吕,但是還沒有處理事件
退出run loop
我們可以通過Core Foundation來添加run loop observers。要創(chuàng)建run loop observer,可以通過CFRunLoopObserverRef來創(chuàng)建新的實例怕午。這個類型會跟蹤你所定義的回調(diào)函數(shù)和所感興趣的活動废登。
與timers類型,run-loop observers可以使用一次或者重復(fù)多次郁惜。一次性的observer會在fire之后自動從run loop移除堡距,而重復(fù)性的observer會繼續(xù)持有。
The Run Loop Sequence Of Events
本小節(jié)講的是RunLoop事件順序兆蕉。每次運行它羽戒,你的線程的run loop處理待處理的事件和給所有attached observers發(fā)出通知。處理的順序如下:
1.通知observers run loop已經(jīng)進(jìn)入
2.通知observers timers準(zhǔn)備要fire
3.通知observers有不是基于port-based的輸入源即將要fire
4.fire任何已經(jīng)準(zhǔn)備好的non-port-based輸入源
5.如果port-based輸入源準(zhǔn)備好且等待fire虎韵,則立即處理這個事件半醉。然后進(jìn)入步驟9
6.通知observers線程即將進(jìn)入睡眠
7.讓線程進(jìn)入睡眠,直到以下任何一種事件到達(dá):
* port-based輸入源有事件到達(dá)
* timer fire
* run loop超時
* run loop被顯式喚醒
8.通知observers線程被喚醒
9.處理待處理的事件:
* 如果用戶定義的timer fired了劝术,處理timer事件并重新啟動循環(huán)缩多。進(jìn)入步驟2
* 如果輸入源fired了呆奕,則傳遞事件
* 如果run loop被顯式喚醒,但是又未超時衬吆,則重啟循環(huán)梁钾,進(jìn)入步驟2
10.通知observers run loop退出
由于observer對timer和輸入源的通知會在事件真正發(fā)生之前被傳遞,這樣就產(chǎn)生了間隙逊抡。如果這個間隙是很關(guān)鍵的姆泻,那么我們可以通過使用sleep和awake-from-sleep通知來幫助我們糾正這個時間間隔問題。
When Would You Use A Run Loop?
什么時候應(yīng)該使用run loop呢冒嫡?
只有當(dāng)我們需要創(chuàng)建子線程的時候拇勃,才會需要到顯示地運行run loop。應(yīng)用程序的主線程的run loop是應(yīng)用啟動的基礎(chǔ)任務(wù)孝凌,在啟動時就會自動啟動run loop方咆。所以我們不需要手動啟動主線程的run loop。
對于子線程蟀架,我們需要確定線程是否需要run loop瓣赂,如果需要,則配置它并啟動它片拍。我們并不問題需要啟動run loop的煌集。比如說,如果我們開一個子線程去執(zhí)行一些長時間的和預(yù)先決定的任務(wù)捌省,我們可能不需要啟動run loop苫纤。Run loop是用于那么需要在線程中有更多地交互的場景。比如說纲缓,我們會在下面的任何一種場景中需要開啟run loop:
使用端口源或者自定義輸入源與其它線程通信
在線程中使用定時器
使用Cocoa中的任何performSelector…方法
保持線程來執(zhí)行周期性的任務(wù)
Using Run Loop Objects
Run Loop對象給添加輸入源方面、定時器和觀察者到run loop提供了主接口。每個線程都有一個單獨的run loop與之關(guān)聯(lián)(對于子線程色徘,若沒有調(diào)用過任何獲取run loop的方法是不會有run loop的,只有調(diào)用過操禀,才會創(chuàng)建或者直接使用)褂策。
在Cocoa中,通過NSRunLoop來創(chuàng)建實例颓屑,在low-level應(yīng)用中斤寂,可以使用CFRunLoopRef類型,它是指針揪惦。
Getting A Run Loop Object
通過以下兩種方式來獲取run loop對象:
在Cocoa中遍搞,使用[NSRunLoop currentRunLoop]獲取
使用CFRunLoopGetCurrent()函數(shù)獲取
配置RunLoop
在子線程運行run loop之前,你必須至少添加一種輸入源或者定時器器腋。如果run loop沒有任何的源需要監(jiān)視溪猿,它就會立刻退出钩杰。
除了添加sources之外,你還可以添加觀察者來檢測runloop不同的執(zhí)行狀態(tài)诊县。要添加觀察者讲弄,可以使用CFRunLoopObserverRef指針類型和使用CFRunLoopAddObserver函數(shù)來添加到run loop中。我們只能通過Core Foundation來創(chuàng)建run loop觀察者依痊,即使是Cocoa應(yīng)用避除。
下面這段代碼展示主線程如何添加觀察者到run loop以及如何創(chuàng)建run loop觀察者:
- (void)threadMain {
// 應(yīng)用程序使用垃圾收集,因此不需要autorelease池。
NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
// 創(chuàng)建一個運行循環(huán)觀察者并將它附加到運行循環(huán)胸嘁。
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);
}
//創(chuàng)建和安排計時器瓶摆。
[NSTimer scheduledTimerWithTimeInterval:0.1 target:self
selector:@selector(doFireTimer:) userInfo:nil repeats:YES];
NSInteger loopCount = 10;
do {
// 運行運行循環(huán)10次讓計時器。
[myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
loopCount--;
} while (loopCount);
}
在為長時間存在的線程配置run-loop時性宏,最好添加至少一個輸入源來接收事件群井。盡管我們可以只用timer源,但是一旦timer調(diào)用后衔沼,經(jīng)常會被invalidate蝌借,這會導(dǎo)致run loop退出。
Starting the Run Loop
只有子線程才有可能需要啟動run loop指蚁。Run loop必須至少有一種輸入源或者timer源來監(jiān)視菩佑。如果沒有任何源,則run loop會退出凝化。
下面的幾種方式可以啟動run loop:
無條件地:無條件進(jìn)入run loop是最簡單的方式稍坯,但也是最不希望這么做的,因為這樣會導(dǎo)致run loop會進(jìn)入永久地循環(huán)搓劫∏朴矗可以添加、刪除輸入源和timer源枪向,但是只能通過kill掉run loop才能停止勤揩。而且還不能使用自定義mode。
限時:與無條件運行run loop不同秘蛔,最好是給run loop添加一個超時時間陨亡。
在特定的mode:除了添加超時時間,還可以指定mode深员。
下面是運行run loop的一段代碼:
- (void)skeletonThreadMain {
//建立一個autorelease如果不使用垃圾收集池负蠕。
BOOL done = NO;
//添加你的來源或計時器運行循環(huán),做其他任何設(shè)置。
do {/ /啟動運行循環(huán)但每個源處理后返回倦畅。
SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);
/ /如果源明確停止運行循環(huán),或如果沒有源或計時器,然后退出遮糖。
if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished))
done = YES;
/ /檢查任何其他退出條件和設(shè)置完成所需的變量。 } while (!done);
/ /清理代碼叠赐。一定要釋放任何生成自動分配池欲账。
}
Exiting the Run Loop
有兩種方法使run loop在處理事件之前屡江,退出run loop:
給run loop設(shè)定超時時間
告訴run loop要stop
設(shè)定超時時間是比較推薦的。我們可以通過CFRunLoopStop函數(shù)來停止run loop敬惦。
Thread Safety and Run Loop Objects
Core Foundation中的Run Loop API是線程安全的(以CF開頭的API)盼理,而Cocoa中的NSRunLoop不是線程安全的。
Configuring Run Loop Sources
下面是展示如何配置不同類型的輸入源俄删。
Defining a Custom Input Source
創(chuàng)建自定義輸入源涉及到以下部分:
想要處理的輸入源的信息
讓感興趣的客戶端知道如何聯(lián)系輸入源的調(diào)度程序
執(zhí)行任何客戶端發(fā)送的請求處理程序
使輸入源失效的取消程序
Timer Source
NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
/ /創(chuàng)建和安排第一個定時器宏怔。
NSDate* futureDate = [NSDate dateWithTimeIntervalSinceNow:1.0];
NSTimer* myTimer = [[NSTimer alloc] initWithFireDate:futureDate
interval:0.1
target:self
selector:@selector(myDoFireTimer1:)
userInfo:nil
[myRunLoop addTimer:myTimer forMode:NSDefaultRunLoopMode];
或者使用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);
最后
本篇文章主要是官方文檔的部分翻譯版本,不過有很多無關(guān)的都省略了畴椰,而且有都轉(zhuǎn)換成筆者的語言來表達(dá)出來臊诊,如果讀不懂,最好還是去看官方文檔吧斜脂。畢竟抓艳,英文與中文翻譯不管怎么翻譯都存在很大的問題。
疑問
官方文檔中提到帚戳,每個線程都有一個run loop與之關(guān)聯(lián)玷或。但是實質(zhì)上子線程在沒有訪問過run loop時,是不存在的片任。當(dāng)訪問時偏友,若不存在則創(chuàng)建run loop并放到全局?jǐn)?shù)組中。筆者閱讀過CFRunLoop.c的源代碼对供,所以當(dāng)有面試官問到這個問題時位他,一定要注意哦!
本文轉(zhuǎn)發(fā)于標(biāo)哥的博客产场,收藏用鹅髓。