線程與RunLoop
線程任務(wù)分兩種断序,一種是直線任務(wù),起點開始拆挥,終點結(jié)束薄霜;另一種是跑圈任務(wù),不斷循環(huán)纸兔,直到通過某種方式將它終止惰瓜。我們都知道:
所有 autorelease 的對象,在出了作用域之后汉矿,會被自動添加到最近創(chuàng)建的自動釋放池中崎坊。
從程序啟動到加載完成是一個完整的運行循環(huán),然后會停下來洲拇,等待用戶交互奈揍,用戶的每一次交互都會啟動一次運行循環(huán),來處理用戶所有的點擊事件呻待、觸摸事件打月。
1.3.4 Run loop的優(yōu)點
一個run loop就是一個事件處理循環(huán),用來不停的監(jiān)聽和處理輸入事件并將其分配到對應(yīng)的目標上進行處理蚕捉。如果僅僅是想實現(xiàn)這個功能奏篙,你可能會想一個簡單的while循環(huán)不就可以實現(xiàn)了嗎,用得著費老大勁來做個那么復(fù)雜的機制迫淹?顯然秘通,蘋果的架構(gòu)設(shè)計師不是吃干飯的,你想到的他們早就想過了敛熬。
首先肺稀,NSRunLoop是一種更加高明的消息處理模式,他就高明在對消息處理過程進行了更好的抽象和封裝应民,這樣才能是的你不用處理一些很瑣碎很低層次的具體消息的處理话原,在NSRunLoop中每一個消息就被打包在input source或者是timer source(見后文)中了。
其次诲锹,也是很重要的一點繁仁,使用run loop可以使你的線程在有工作的時候工作,沒有工作的時候休眠归园,這可以大大節(jié)省系統(tǒng)資源黄虱。
二、Run loop相關(guān)知識點
2.1輸入事件來源
Run loop接收輸入事件來自兩種不同的來源:輸入源(input source)和定時源(timer source)庸诱。兩種源都使用程序的某一特定的處理例程來處理到達的事件捻浦。圖-1顯示了run loop的概念結(jié)構(gòu)以及各種源晤揣。
需要說明的是,當你創(chuàng)建輸入源朱灿,你需要將其分配給run loop中的一個或多個模式(什么是模式昧识,下文將會講到)。模式只會在特定事件影響監(jiān)聽的源母剥。大多數(shù)情況下滞诺,run loop運行在默認模式下,但是你也可以使其運行在自定義模式环疼。若某一源在當前模式下不被監(jiān)聽习霹,那么任何其生成的消息只在run loop運行在其關(guān)聯(lián)的模式下才會被傳遞。
圖-1 Runloop的結(jié)構(gòu)和輸入源類型
2.1.1輸入源(input source)
傳遞異步事件炫隶,通常消息來自于其他線程或程序淋叶。輸入源傳遞異步消息給相應(yīng)的處理例程,并調(diào)用runUntilDate:方法來退出(在線程里面相關(guān)的NSRunLoop對象調(diào)用)伪阶。
2.1.1.1基于端口的輸入源
基于端口的輸入源由內(nèi)核自動發(fā)送煞檩。
Cocoa和Core Foundation內(nèi)置支持使用端口相關(guān)的對象和函數(shù)來創(chuàng)建的基于端口的源。例如栅贴,在Cocoa里面你從來不需要直接創(chuàng)建輸入源斟湃。你只要簡單的創(chuàng)建端口對象,并使用NSPort的方法把該端口添加到run loop檐薯。端口對象會自己處理創(chuàng)建和配置輸入源凝赛。
在Core Foundation,你必須人工創(chuàng)建端口和它的run loop源坛缕。我們可以使用端口相關(guān)的函數(shù)(CFMachPortRef墓猎,CFMessagePortRef,CFSocketRef)來創(chuàng)建合適的對象赚楚。下面的例子展示了如何創(chuàng)建一個基于端口的輸入源毙沾,將其添加到run loop并啟動:
voidcreatePortSource()
{
CFMessagePortRef port = CFMessagePortCreateLocal(kCFAllocatorDefault,CFSTR("com.someport"),myCallbackFunc, NULL,NULL);
CFRunLoopSourceRef source = CFMessagePortCreateRunLoopSource(kCFAllocatorDefault, port,0);
CFRunLoopAddSource(CFRunLoopGetCurrent(), source,kCFRunLoopCommonModes);
while (pageStillLoading) {
NSAutoreleasePool *pool = [[NSAutoreleasePoolalloc] init];
CFRunLoopRun();
[pool release];
}
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source,kCFRunLoopDefaultMode);
CFRelease(source);
}
2.1.1.2自定義輸入源
自定義的輸入源需要人工從其他線程發(fā)送。
為了創(chuàng)建自定義輸入源宠页,必須使用Core Foundation里面的CFRunLoopSourceRef類型相關(guān)的函數(shù)來創(chuàng)建左胞。你可以使用回調(diào)函數(shù)來配置自定義輸入源。Core Fundation會在配置源的不同地方調(diào)用回調(diào)函數(shù)举户,處理輸入事件烤宙,在源從run loop移除的時候清理它。
除了定義在事件到達時自定義輸入源的行為敛摘,你也必須定義消息傳遞機制。源的這部分運行在單獨的線程里面乳愉,并負責(zé)在數(shù)據(jù)等待處理的時候傳遞數(shù)據(jù)給源并通知它處理數(shù)據(jù)兄淫。消息傳遞機制的定義取決于你屯远,但最好不要過于復(fù)雜。創(chuàng)建并啟動自定義輸入源的示例如下:
voidcreateCustomSource()
{
CFRunLoopSourceContext context = {0,NULL, NULL,NULL, NULL,NULL, NULL,NULL, NULL,NULL};
CFRunLoopSourceRef source =CFRunLoopSourceCreate(kCFAllocatorDefault,0, &context);
CFRunLoopAddSource(CFRunLoopGetCurrent(), source,kCFRunLoopDefaultMode);
while (pageStillLoading) {
NSAutoreleasePool *pool = [[NSAutoreleasePoolalloc] init];
CFRunLoopRun();
[pool release];
}
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source,kCFRunLoopDefaultMode);
CFRelease(source);
}
2.1.1.3Cocoa上的Selector源
除了基于端口的源捕虽,Cocoa定義了自定義輸入源慨丐,允許你在任何線程執(zhí)行selector方法。和基于端口的源一樣泄私,執(zhí)行selector請求會在目標線程上序列化房揭,減緩許多在線程上允許多個方法容易引起的同步問題。不像基于端口的源晌端,一個selector執(zhí)行完后會自動從run loop里面移除捅暴。
當在其他線程上面執(zhí)行selector時,目標線程須有一個活動的run loop咧纠。對于你創(chuàng)建的線程蓬痒,這意味著線程在你顯式的啟動run loop之前是不會執(zhí)行selector方法的,而是一直處于休眠狀態(tài)漆羔。
NSObject類提供了類似如下的selector方法:
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)argwaitUntilDone:(BOOL)wait modes:(NSArray *)array;
2.1.2定時源(timer source)
定時源在預(yù)設(shè)的時間點同步方式傳遞消息梧奢,這些消息都會發(fā)生在特定時間或者重復(fù)的時間間隔。定時源則直接傳遞消息給處理例程演痒,不會立即退出run loop亲轨。
需要注意的是,盡管定時器可以產(chǎn)生基于時間的通知鸟顺,但它并不是實時機制惦蚊。和輸入源一樣,定時器也和你的run loop的特定模式相關(guān)诊沪。如果定時器所在的模式當前未被run loop監(jiān)視养筒,那么定時器將不會開始直到run loop運行在相應(yīng)的模式下。類似的端姚,如果定時器在run loop處理某一事件期間開始晕粪,定時器會一直等待直到下次run loop開始相應(yīng)的處理程序。如果run loop不再運行渐裸,那定時器也將永遠不啟動巫湘。
創(chuàng)建定時器源有兩種方法,
方法一:
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:4.0
target:self
selector:@selector(backgroundThreadFire:) userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timerforMode:NSDefaultRunLoopMode];
方法二:
[NSTimer scheduledTimerWithTimeInterval:10
target:self
selector:@selector(backgroundThreadFire:)
userInfo:nil
repeats:YES];
2.2 RunLoop觀察者
源是在合適的同步或異步事件發(fā)生時觸發(fā)昏鹃,而run loop觀察者則是在run loop本身運行的特定時候觸發(fā)尚氛。你可以使用run loop觀察者來為處理某一特定事件或是進入休眠的線程做準備。你可以將run loop觀察者和以下事件關(guān)聯(lián):
- Runloop入口
- Runloop何時處理一個定時器
- Runloop何時處理一個輸入源
- Runloop何時進入睡眠狀態(tài)
- Runloop何時被喚醒洞渤,但在喚醒之前要處理的事件
- Runloop終止
和定時器類似阅嘶,在創(chuàng)建的時候你可以指定run loop觀察者可以只用一次或循環(huán)使用。若只用一次,那么在它啟動后讯柔,會把它自己從run loop里面移除抡蛙,而循環(huán)的觀察者則不會。定義觀察者并把它添加到run loop魂迄,只能使用Core Fundation剑勾。下面的例子演示了如何創(chuàng)建run loop的觀察者:
-
(void)addObserverToCurrentRunloop
{
// The application uses garbage collection, so noautorelease pool is needed.
NSRunLoop*myRunLoop = [NSRunLoop currentRunLoop];// Create a run loop observer and attach it to the runloop.
CFRunLoopObserverContext context = {0,self, NULL,NULL, NULL};
CFRunLoopObserverRef observer =CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopBeforeTimers,YES, 0, &myRunLoopObserver, &context);if (observer)
{
CFRunLoopRef cfLoop = [myRunLoopgetCFRunLoop];
CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
}
}
其中渊胸,kCFRunLoopBeforeTimers表示選擇監(jiān)聽定時器觸發(fā)前處理事件虽缕,后面的YES表示循環(huán)監(jiān)聽仪壮。
2.3 RunLoop的事件隊列
每次運行run loop,你線程的run loop對會自動處理之前未處理的消息湿酸,并通知相關(guān)的觀察者婿屹。具體的順序如下:
通知觀察者run loop已經(jīng)啟動
通知觀察者任何即將要開始的定時器
通知觀察者任何即將啟動的非基于端口的源
啟動任何準備好的非基于端口的源
如果基于端口的源準備好并處于等待狀態(tài),立即啟動稿械;并進入步驟9选泻。
通知觀察者線程進入休眠
將線程置于休眠直到任一下面的事件發(fā)生:某一事件到達基于端口的源
定時器啟動
Run loop設(shè)置的時間已經(jīng)超時
run loop被顯式喚醒
通知觀察者線程將被喚醒。
處理未處理的事件如果用戶定義的定時器啟動美莫,處理定時器事件并重啟run loop页眯。進入步驟2
如果輸入源啟動,傳遞相應(yīng)的消息
如果run loop被顯式喚醒而且時間還沒超時厢呵,重啟run loop窝撵。進入步驟2
通知觀察者run loop結(jié)束。
因為定時器和輸入源的觀察者是在相應(yīng)的事件發(fā)生之前傳遞消息襟铭,所以通知的時間和實際事件發(fā)生的時間之間可能存在誤差碌奉。如果需要精確時間控制,你可以使用休眠和喚醒通知來幫助你校對實際發(fā)生事件的時間寒砖。
因為當你運行run loop時定時器和其它周期性事件經(jīng)常需要被傳遞赐劣,撤銷run loop也會終止消息傳遞。典型的例子就是鼠標路徑追蹤哩都。因為你的代碼直接獲取到消息而不是經(jīng)由程序傳遞魁兼,因此活躍的定時器不會開始直到鼠標追蹤結(jié)束并將控制權(quán)交給程序。
Run loop可以由run loop對象顯式喚醒漠嵌。其它消息也可以喚醒run loop咐汞。例如,添加新的非基于端口的源會喚醒run loop從而可以立即處理輸入源而不需要等待其他事件發(fā)生后再處理儒鹿。
從這個事件隊列中可以看出:
①如果是事件到達化撕,消息會被傳遞給相應(yīng)的處理程序來處理, runloop處理完當次事件后约炎,run loop會退出植阴,而不管之前預(yù)定的時間到了沒有。你可以重新啟動run loop來等待下一事件。
②如果線程中有需要處理的源掠手,但是響應(yīng)的事件沒有到來的時候热芹,線程就會休眠等待相應(yīng)事件的發(fā)生。這就是為什么run loop可以做到讓線程有工作的時候忙于工作惨撇,而沒工作的時候處于休眠狀態(tài)。
2.4什么時候使用run loop
僅當在為你的程序創(chuàng)建輔助線程的時候府寒,你才需要顯式運行一個run loop魁衙。Run loop是程序主線程基礎(chǔ)設(shè)施的關(guān)鍵部分。所以株搔,Cocoa和Carbon程序提供了代碼運行主程序的循環(huán)并自動啟動run loop剖淀。IOS程序中UIApplication的run方法(或Mac OS X中的NSApplication)作為程序啟動步驟的一部分,它在程序正常啟動的時候就會啟動程序的主循環(huán)纤房。類似的纵隔,RunApplicationEventLoop函數(shù)為Carbon程序啟動主循環(huán)。如果你使用xcode提供的模板創(chuàng)建你的程序炮姨,那你永遠不需要自己去顯式的調(diào)用這些例程捌刮。
對于輔助線程,你需要判斷一個run loop是否是必須的舒岸。如果是必須的绅作,那么你要自己配置并啟動它。你不需要在任何情況下都去啟動一個線程的run loop蛾派。比如俄认,你使用線程來處理一個預(yù)先定義的長時間運行的任務(wù)時,你應(yīng)該避免啟動run loop洪乍。Run loop在你要和線程有更多的交互時才需要眯杏,比如以下情況:
使用端口或自定義輸入源來和其他線程通信
使用線程的定時器
Cocoa中使用任何performSelector…的方法
使線程周期性工作
如果你決定在程序中使用run loop,那么它的配置和啟動都很簡單壳澳。和所有線程編程一樣岂贩,你需要計劃好在輔助線程退出線程的情形。讓線程自然退出往往比強制關(guān)閉它更好钾埂。