做了一年多的IOS開發(fā)偿乖,對IOS和Objective-C深層次的了解還十分有限冶伞,大多還停留在會用API的級別,這是件挺可悲的事情筑凫。想學(xué)好一門語言還是需要深層次的了解它,這樣才能在使用的時候得心應(yīng)手并村,出現(xiàn)各種怪異的問題時不至于不知所措巍实。廢話少說,進(jìn)入今天的正題哩牍。
不知道大家有沒有想過這個問題棚潦,一個應(yīng)用開始運行以后放在那里,如果不對它進(jìn)行任何操作膝昆,這個應(yīng)用就像靜止了一樣丸边,不會自發(fā)的有任何動作發(fā)生,但是如果我們點擊界面上的一個按鈕荚孵,這個時候就會有對應(yīng)的按鈕響應(yīng)事件發(fā)生妹窖。給我們的感覺就像應(yīng)用一直處于隨時待命的狀態(tài),在沒人操作的時候它一直在休息收叶,在讓它干活的時候骄呼,它就能立刻響應(yīng)。其實,這就是run loop的功勞谒麦。
一俄讹、線程與run loop
- 1.1 線程任務(wù)的類型
再來說說線程。有些線程執(zhí)行的任務(wù)是一條直線绕德,起點到終點患膛;而另一些線程要干的活則是一個圓,不斷循環(huán)耻蛇,直到通過某種方式將它終止踪蹬。直線線程如簡單的Hello World,運行打印完,它的生命周期便結(jié)束了臣咖,像曇花一現(xiàn)那樣跃捣;圓類型的如操作系統(tǒng),一直運行直到你關(guān)機(jī)夺蛇。在IOS中疚漆,圓型的線程就是通過run loop不停的循環(huán)實現(xiàn)的。
- 1.2 線程與run loop的關(guān)系
Run loop刁赦,正如其名娶聘,loop表示某種循環(huán),和run放在一起就表示一直在運行著的循環(huán)甚脉。實際上丸升,run loop和線程是緊密相連的冒冬,可以這樣說run loop是為了線程而生空民,沒有線程,它就沒有存在的必要挑胸。Run loops是線程的基礎(chǔ)架構(gòu)部分猴凹,Cocoa和CoreFundation都提供了run loop對象方便配置和管理線程的run loop(以下都已Cocoa為例)夷狰。每個線程,包括程序的主線程(main thread)都有與之相應(yīng)的run loop對象郊霎。 - 1.2.1 主線程的run loop默認(rèn)是啟動的孵淘。
iOS的應(yīng)用程序里面,程序啟動后會有一個如下的main() 函數(shù):
int main(int argc, char *argv[])
{
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([appDelegate class]));
}
}
重點是UIApplicationMain() 函數(shù)歹篓,這個方法會為main thread 設(shè)置一個NSRunLoop 對象,這就解釋了本文開始說的為什么我們的應(yīng)用可以在無人操作的時候休息揉阎,需要讓它干活的時候又能立馬響應(yīng)庄撮。
- 1.2.2 對其它線程來說,run loop默認(rèn)是沒有啟動的毙籽,如果你需要更多的線程交互則可以手動配置和啟動洞斯,如果線程只是去執(zhí)行一個長時間的已確定的任務(wù)則不需要。
- 1.2.3 在任何一個Cocoa程序的線程中,都可以通過:
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
來獲取到當(dāng)前線程的run loop烙如。
- 1.3 關(guān)于run loop的幾點說明
- 1.3.1 Cocoa中的NSRunLoop類并不是線程安全的
我們不能再一個線程中去操作另外一個線程的run loop對象么抗,那很可能會造成意想不到的后果。不過幸運的是CoreFundation中的不透明類CFRunLoopRef是線程安全的亚铁,而且兩種類型的run loop完全可以混合使用蝇刀。Cocoa中的NSRunLoop類可以通過實例方法:
- (CFRunLoopRef)getCFRunLoop;
獲取對應(yīng)的CFRunLoopRef類,來達(dá)到線程安全的目的徘溢。
- 1.3.2 Run loop的管理并不完全是自動的吞琐。
我們?nèi)员仨氃O(shè)計線程代碼以在適當(dāng)?shù)臅r候啟動run loop并正確響應(yīng)輸入事件,當(dāng)然前提是線程中需要用到run loop然爆。而且站粟,我們還需要使用while/for語句來驅(qū)動run loop能夠循環(huán)運行,下面的代碼就成功驅(qū)動了一個run loop:
BOOL isRunning = NO;
do {
isRunning = [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDatedistantFuture]];
} while (isRunning);
- 1.3.3 Run loop同時也負(fù)責(zé)autorelease pool的創(chuàng)建和釋放
在使用手動的內(nèi)存管理方式的項目中曾雕,會經(jīng)常用到很多自動釋放的對象奴烙,如果這些對象不能夠被即時釋放掉,會造成內(nèi)存占用量急劇增大剖张。Run loop就為我們做了這樣的工作切诀,每當(dāng)一個運行循環(huán)結(jié)束的時候,它都會釋放一次autorelease pool修械,同時pool中的所有自動釋放類型變量都會被釋放掉趾牧。 - 1.3.4 Run loop的優(yōu)點
一個run loop就是一個事件處理循環(huán),用來不停的監(jiān)聽和處理輸入事件并將其分配到對應(yīng)的目標(biāo)上進(jìn)行處理肯污。如果僅僅是想實現(xiàn)這個功能翘单,你可能會想一個簡單的while循環(huán)不就可以實現(xiàn)了嗎,用得著費老大勁來做個那么復(fù)雜的機(jī)制蹦渣?顯然哄芜,蘋果的架構(gòu)設(shè)計師不是吃干飯的,你想到的他們早就想過了柬唯。
首先认臊,NSRunLoop是一種更加高明的消息處理模式,他就高明在對消息處理過程進(jìn)行了更好的抽象和封裝锄奢,這樣才能是的你不用處理一些很瑣碎很低層次的具體消息的處理失晴,在NSRunLoop中每一個消息就被打包在input source或者是timer source(見后文)中了。
其次拘央,也是很重要的一點涂屁,使用run loop可以使你的線程在有工作的時候工作,沒有工作的時候休眠灰伟,這可以大大節(jié)省系統(tǒng)資源拆又。
二、Run loop相關(guān)知識點
2.1輸入事件來源
Run loop接收輸入事件來自兩種不同的來源:輸入源(input source)和定時源(timer source)。兩種源都使用程序的某一特定的處理例程來處理到達(dá)的事件帖族。圖-1顯示了run loop的概念結(jié)構(gòu)以及各種源栈源。需要說明的是,當(dāng)你創(chuàng)建輸入源竖般,你需要將其分配給run loop中的一個或多個模式(什么是模式甚垦,下文將會講到)。模式只會在特定事件影響監(jiān)聽的源捻激。大多數(shù)情況下制轰,run loop運行在默認(rèn)模式下,但是你也可以使其運行在自定義模式胞谭。若某一源在當(dāng)前模式下不被監(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 = [[NSAutoreleasePool alloc] 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移除的時候清理它。
除了定義在事件到達(dá)時自定義輸入源的行為念链,你也必須定義消息傳遞機(jī)制。源的這部分運行在單獨的線程里面,并負(fù)責(zé)在數(shù)據(jù)等待處理的時候傳遞數(shù)據(jù)給源并通知它處理數(shù)據(jù)掂墓。消息傳遞機(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 = [[NSAutoreleasePool alloc] init];
CFRunLoopRun();
[pool release];
}
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
CFRelease(source);
}
- 2.1.1.3Cocoa上的Selector源
除了基于端口的源君编,Cocoa定義了自定義輸入源跨嘉,允許你在任何線程執(zhí)行selector方法。和基于端口的源一樣吃嘿,執(zhí)行selector請求會在目標(biāo)線程上序列化祠乃,減緩許多在線程上允許多個方法容易引起的同步問題。不像基于端口的源兑燥,一個selector執(zhí)行完后會自動從run loop里面移除亮瓷。
當(dāng)在其他線程上面執(zhí)行selector時,目標(biāo)線程須有一個活動的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)生基于時間的通知短荐,但它并不是實時機(jī)制倚舀。和輸入源一樣,定時器也和你的run loop的特定模式相關(guān)搓侄。如果定時器所在的模式當(dāng)前未被run loop監(jiān)視瞄桨,那么定時器將不會開始直到run loop運行在相應(yīng)的模式下。類似的讶踪,如果定時器在run loop處理某一事件期間開始芯侥,定時器會一直等待直到下次run loop開始相應(yīng)的處理程序。如果run loop不再運行乳讥,那定時器也將永遠(yuǎn)不啟動柱查。
創(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觀察者來為處理某一特定事件或是進(jìn)入休眠的線程做準(zhǔn)備。你可以將run loop觀察者和以下事件關(guān)聯(lián):
1. Runloop入口
2. Runloop何時處理一個定時器
3. Runloop何時處理一個輸入源
4. Runloop何時進(jìn)入睡眠狀態(tài)
5. Runloop何時被喚醒汹忠,但在喚醒之前要處理的事件
6. 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.
// 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 = [myRunLoop getCFRunLoop];
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)啟動
通知觀察者任何即將要開始的定時器
通知觀察者任何即將啟動的非基于端口的源
啟動任何準(zhǔn)備好的非基于端口的源
如果基于端口的源準(zhǔn)備好并處于等待狀態(tài),立即啟動欢峰;并進(jìn)入步驟9葬荷。
通知觀察者線程進(jìn)入休眠
將線程置于休眠直到任一下面的事件發(fā)生:
某一事件到達(dá)基于端口的源
定時器啟動
Run loop設(shè)置的時間已經(jīng)超時
run loop被顯式喚醒
通知觀察者線程將被喚醒。
處理未處理的事件
如果用戶定義的定時器啟動纽帖,處理定時器事件并重啟run loop宠漩。進(jìn)入步驟2
如果輸入源啟動,傳遞相應(yīng)的消息
如果run loop被顯式喚醒而且時間還沒超時懊直,重啟run loop扒吁。進(jìn)入步驟2
通知觀察者run loop結(jié)束。
因為定時器和輸入源的觀察者是在相應(yīng)的事件發(fā)生之前傳遞消息室囊,所以通知的時間和實際事件發(fā)生的時間之間可能存在誤差雕崩。如果需要精確時間控制,你可以使用休眠和喚醒通知來幫助你校對實際發(fā)生事件的時間融撞。
因為當(dāng)你運行run loop時定時器和其它周期性事件經(jīng)常需要被傳遞盼铁,撤銷run loop也會終止消息傳遞。典型的例子就是鼠標(biāo)路徑追蹤尝偎。因為你的代碼直接獲取到消息而不是經(jīng)由程序傳遞饶火,因此活躍的定時器不會開始直到鼠標(biāo)追蹤結(jié)束并將控制權(quán)交給程序。
Run loop可以由run loop對象顯式喚醒致扯。其它消息也可以喚醒run loop肤寝。例如,添加新的非基于端口的源會喚醒run loop從而可以立即處理輸入源而不需要等待其他事件發(fā)生后再處理抖僵。
從這個事件隊列中可以看出:①如果是事件到達(dá)鲤看,消息會被傳遞給相應(yīng)的處理程序來處理, runloop處理完當(dāng)次事件后耍群,run loop會退出义桂,而不管之前預(yù)定的時間到了沒有找筝。你可以重新啟動run loop來等待下一事件。
②如果線程中有需要處理的源慷吊,但是響應(yīng)的事件沒有到來的時候呻征,線程就會休眠等待相應(yīng)事件的發(fā)生。這就是為什么run loop可以做到讓線程有工作的時候忙于工作罢浇,而沒工作的時候處于休眠狀態(tài)。
- 2.4什么時候使用run loop
僅當(dāng)在為你的程序創(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)建你的程序吵聪,那你永遠(yuǎn)不需要自己去顯式的調(diào)用這些例程凌那。
對于輔助線程,你需要判斷一個run loop是否是必須的吟逝。如果是必須的帽蝶,那么你要自己配置并啟動它。你不需要在任何情況下都去啟動一個線程的run loop块攒。比如励稳,你使用線程來處理一個預(yù)先定義的長時間運行的任務(wù)時,你應(yīng)該避免啟動run loop囱井。Run loop在你要和線程有更多的交互時才需要驹尼,比如以下情況:
使用端口或自定義輸入源來和其他線程通信
使用線程的定時器
Cocoa中使用任何performSelector…的方法
使線程周期性工作
如果你決定在程序中使用run loop,那么它的配置和啟動都很簡單庞呕。和所有線程編程一樣新翎,你需要計劃好在輔助線程退出線程的情形。讓線程自然退出往往比強(qiáng)制關(guān)閉它更好千扶。