Objective-C之run loop详解
做了一年多的IOS開發(fā),對(duì)IOS和Objective-C深層次的了解還十分有限,大多還停留在會(huì)用API的級(jí)別,這是件挺可悲的事情。想學(xué)好一門語言還是需要深層次的了解它,這樣才能在使用的時(shí)候得心應(yīng)手,出現(xiàn)各種怪異的問題時(shí)不至于不知所措。廢話少說,進(jìn)入今天的正題。
不知道大家有沒有想過這個(gè)問題,一個(gè)應(yīng)用開始運(yùn)行以后放在那里,如果不對(duì)它進(jìn)行任何操作,這個(gè)應(yīng)用就像靜止了一樣,不會(huì)自發(fā)的有任何動(dòng)作發(fā)生,但是如果我們點(diǎn)擊界面上的一個(gè)按鈕,這個(gè)時(shí)候就會(huì)有對(duì)應(yīng)的按鈕響應(yīng)事件發(fā)生。給我們的感覺就像應(yīng)用一直處于隨時(shí)待命的狀態(tài),在沒人操作的時(shí)候它一直在休息,在讓它干活的時(shí)候,它就能立刻響應(yīng)。其實(shí),這就是run loop的功勞。
一、線程與run loop
1.1?線程任務(wù)的類型
再來說說線程。有些線程執(zhí)行的任務(wù)是一條直線,起點(diǎn)到終點(diǎn);而另一些線程要干的活則是一個(gè)圓,不斷循環(huán),直到通過某種方式將它終止。直線線程如簡(jiǎn)單的Hello World,運(yùn)行打印完,它的生命周期便結(jié)束了,像曇花一現(xiàn)那樣;圓類型的如操作系統(tǒng),一直運(yùn)行直到你關(guān)機(jī)。在IOS中,圓型的線程就是通過run loop不停的循環(huán)實(shí)現(xiàn)的。
1.2?線程與run loop的關(guān)系
Run loop,正如其名,loop表示某種循環(huán),和run放在一起就表示一直在運(yùn)行著的循環(huán)。實(shí)際上,run loop和線程是緊密相連的,可以這樣說run loop是為了線程而生,沒有線程,它就沒有存在的必要。Run loops是線程的基礎(chǔ)架構(gòu)部分,Cocoa和CoreFundation都提供了run loop對(duì)象方便配置和管理線程的run loop(以下都已Cocoa為例)。每個(gè)線程,包括程序的主線程(main thread)都有與之相應(yīng)的run loop對(duì)象。
1.2.1?主線程的run loop默認(rèn)是啟動(dòng)的。
iOS的應(yīng)用程序里面,程序啟動(dòng)后會(huì)有一個(gè)如下的main()?函數(shù):
???? int?main(int?argc,?char?*argv[])
???? {
?? ??????? ?@autoreleasepool?{
????? ????? ??return?UIApplicationMain(argc, argv,?nil,?NSStringFromClass([appDelegate?class]));
??? ?????? }
? }
重點(diǎn)是UIApplicationMain()?函數(shù),這個(gè)方法會(huì)為main thread?設(shè)置一個(gè)NSRunLoop?對(duì)象,這就解釋了本文開始說的為什么我們的應(yīng)用可以在無人操作的時(shí)候休息,需要讓它干活的時(shí)候又能立馬響應(yīng)。
1.2.2?對(duì)其它線程來說,run loop默認(rèn)是沒有啟動(dòng)的,如果你需要更多的線程交互則可以手動(dòng)配置和啟動(dòng),如果線程只是去執(zhí)行一個(gè)長(zhǎng)時(shí)間的已確定的任務(wù)則不需要。
1.2.3?在任何一個(gè)Cocoa程序的線程中,都可以通過:
NSRunLoop?? *runloop = [NSRunLoop?currentRunLoop];
來獲取到當(dāng)前線程的run loop。
1.3?關(guān)于run loop的幾點(diǎn)說明
1.3.1 Cocoa中的NSRunLoop類并不是線程安全的
我們不能再一個(gè)線程中去操作另外一個(gè)線程的run loop對(duì)象,那很可能會(huì)造成意想不到的后果。不過幸運(yùn)的是CoreFundation中的不透明類CFRunLoopRef是線程安全的,而且兩種類型的run loop完全可以混合使用。Cocoa中的NSRunLoop類可以通過實(shí)例方法:
- (CFRunLoopRef)getCFRunLoop;
獲取對(duì)應(yīng)的CFRunLoopRef類,來達(dá)到線程安全的目的。
1.3.2 Run loop的管理并不完全是自動(dòng)的。
我們?nèi)员仨氃O(shè)計(jì)線程代碼以在適當(dāng)?shù)臅r(shí)候啟動(dòng)run loop并正確響應(yīng)輸入事件,當(dāng)然前提是線程中需要用到run loop。而且,我們還需要使用while/for語句來驅(qū)動(dòng)run loop能夠循環(huán)運(yùn)行,下面的代碼就成功驅(qū)動(dòng)了一個(gè)run loop:
???? BOOL?isRunning =?NO;
??????do?{
??????????? isRunning = [[NSRunLoop?currentRunLoop]?runMode:NSDefaultRunLoopMode?beforeDate:[NSDatedistantFuture]];
???? }?while?(isRunning);
1.3.3 Run loop同時(shí)也負(fù)責(zé)autorelease pool的創(chuàng)建和釋放
在使用手動(dòng)的內(nèi)存管理方式的項(xiàng)目中,會(huì)經(jīng)常用到很多自動(dòng)釋放的對(duì)象,如果這些對(duì)象不能夠被即時(shí)釋放掉,會(huì)造成內(nèi)存占用量急劇增大。Run loop就為我們做了這樣的工作,每當(dāng)一個(gè)運(yùn)行循環(huán)結(jié)束的時(shí)候,它都會(huì)釋放一次autorelease pool,同時(shí)pool中的所有自動(dòng)釋放類型變量都會(huì)被釋放掉。
1.3.4 Run loop的優(yōu)點(diǎn)
一個(gè)run loop就是一個(gè)事件處理循環(huán),用來不停的監(jiān)聽和處理輸入事件并將其分配到對(duì)應(yīng)的目標(biāo)上進(jìn)行處理。如果僅僅是想實(shí)現(xiàn)這個(gè)功能,你可能會(huì)想一個(gè)簡(jiǎn)單的while循環(huán)不就可以實(shí)現(xiàn)了嗎,用得著費(fèi)老大勁來做個(gè)那么復(fù)雜的機(jī)制?顯然,蘋果的架構(gòu)設(shè)計(jì)師不是吃干飯的,你想到的他們?cè)缇拖脒^了。
首先,NSRunLoop是一種更加高明的消息處理模式,他就高明在對(duì)消息處理過程進(jìn)行了更好的抽象和封裝,這樣才能是的你不用處理一些很瑣碎很低層次的具體消息的處理,在NSRunLoop中每一個(gè)消息就被打包在input source或者是timer source(見后文)中了。
其次,也是很重要的一點(diǎn),使用run loop可以使你的線程在有工作的時(shí)候工作,沒有工作的時(shí)候休眠,這可以大大節(jié)省系統(tǒng)資源。
?
二、Run loop相關(guān)知識(shí)點(diǎn)
2.1輸入事件來源
Run loop接收輸入事件來自兩種不同的來源:輸入源(input source)和定時(shí)源(timer source)。兩種源都使用程序的某一特定的處理例程來處理到達(dá)的事件。圖-1顯示了run loop的概念結(jié)構(gòu)以及各種源。
需要說明的是,當(dāng)你創(chuàng)建輸入源,你需要將其分配給run loop中的一個(gè)或多個(gè)模式(什么是模式,下文將會(huì)講到)。模式只會(huì)在特定事件影響監(jiān)聽的源。大多數(shù)情況下,run loop運(yùn)行在默認(rèn)模式下,但是你也可以使其運(yùn)行在自定義模式。若某一源在當(dāng)前模式下不被監(jiān)聽,那么任何其生成的消息只在run loop運(yùn)行在其關(guān)聯(lián)的模式下才會(huì)被傳遞。
圖-1? Runloop的結(jié)構(gòu)和輸入源類型
?
2.1.1輸入源(input source)
傳遞異步事件,通常消息來自于其他線程或程序。輸入源傳遞異步消息給相應(yīng)的處理例程,并調(diào)用runUntilDate:方法來退出(在線程里面相關(guān)的NSRunLoop對(duì)象調(diào)用)。
2.1.1.1基于端口的輸入源
基于端口的輸入源由內(nèi)核自動(dòng)發(fā)送。
Cocoa和Core Foundation內(nèi)置支持使用端口相關(guān)的對(duì)象和函數(shù)來創(chuàng)建的基于端口的源。例如,在Cocoa里面你從來不需要直接創(chuàng)建輸入源。你只要簡(jiǎn)單的創(chuàng)建端口對(duì)象,并使用NSPort的方法把該端口添加到run loop。端口對(duì)象會(huì)自己處理創(chuàng)建和配置輸入源。
在Core Foundation,你必須人工創(chuàng)建端口和它的run loop源。我們可以使用端口相關(guān)的函數(shù)(CFMachPortRef,CFMessagePortRef,CFSocketRef)來創(chuàng)建合適的對(duì)象。下面的例子展示了如何創(chuàng)建一個(gè)基于端口的輸入源,將其添加到run loop并啟動(dòng):
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會(huì)在配置源的不同地方調(diào)用回調(diào)函數(shù),處理輸入事件,在源從run loop移除的時(shí)候清理它。
除了定義在事件到達(dá)時(shí)自定義輸入源的行為,你也必須定義消息傳遞機(jī)制。源的這部分運(yùn)行在單獨(dú)的線程里面,并負(fù)責(zé)在數(shù)據(jù)等待處理的時(shí)候傳遞數(shù)據(jù)給源并通知它處理數(shù)據(jù)。消息傳遞機(jī)制的定義取決于你,但最好不要過于復(fù)雜。創(chuàng)建并啟動(dò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請(qǐng)求會(huì)在目標(biāo)線程上序列化,減緩許多在線程上允許多個(gè)方法容易引起的同步問題。不像基于端口的源,一個(gè)selector執(zhí)行完后會(huì)自動(dòng)從run loop里面移除。
當(dāng)在其他線程上面執(zhí)行selector時(shí),目標(biāo)線程須有一個(gè)活動(dòng)的run loop。對(duì)于你創(chuàng)建的線程,這意味著線程在你顯式的啟動(dòng)run loop之前是不會(huì)執(zhí)行selector方法的,而是一直處于休眠狀態(tài)。
NSObject類提供了類似如下的selector方法:
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)argwaitUntilDone:(BOOL)wait modes:(NSArray?*)array;
?
2.1.2定時(shí)源(timer source)
定時(shí)源在預(yù)設(shè)的時(shí)間點(diǎn)同步方式傳遞消息,這些消息都會(huì)發(fā)生在特定時(shí)間或者重復(fù)的時(shí)間間隔。定時(shí)源則直接傳遞消息給處理例程,不會(huì)立即退出run loop。
需要注意的是,盡管定時(shí)器可以產(chǎn)生基于時(shí)間的通知,但它并不是實(shí)時(shí)機(jī)制。和輸入源一樣,定時(shí)器也和你的run loop的特定模式相關(guān)。如果定時(shí)器所在的模式當(dāng)前未被run loop監(jiān)視,那么定時(shí)器將不會(huì)開始直到run loop運(yùn)行在相應(yīng)的模式下。類似的,如果定時(shí)器在run loop處理某一事件期間開始,定時(shí)器會(huì)一直等待直到下次run loop開始相應(yīng)的處理程序。如果run loop不再運(yùn)行,那定時(shí)器也將永遠(yuǎn)不啟動(dòng)。
創(chuàng)建定時(shí)器源有兩種方法,
方法一:
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ā)生時(shí)觸發(fā),而run loop觀察者則是在run loop本身運(yùn)行的特定時(shí)候觸發(fā)。你可以使用run loop觀察者來為處理某一特定事件或是進(jìn)入休眠的線程做準(zhǔn)備。你可以將run loop觀察者和以下事件關(guān)聯(lián):
1.? Runloop入口
2.? Runloop何時(shí)處理一個(gè)定時(shí)器
3.? Runloop何時(shí)處理一個(gè)輸入源
4.? Runloop何時(shí)進(jìn)入睡眠狀態(tài)
5.? Runloop何時(shí)被喚醒,但在喚醒之前要處理的事件
6.? Runloop終止
和定時(shí)器類似,在創(chuàng)建的時(shí)候你可以指定run loop觀察者可以只用一次或循環(huán)使用。若只用一次,那么在它啟動(dòng)后,會(huì)把它自己從run loop里面移除,而循環(huán)的觀察者則不會(huì)。定義觀察者并把它添加到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 = [myRunLoop?getCFRunLoop];
???????CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
??? }
}
其中,kCFRunLoopBeforeTimers表示選擇監(jiān)聽定時(shí)器觸發(fā)前處理事件,后面的YES表示循環(huán)監(jiān)聽。
?
2.3?RunLoop的事件隊(duì)列
每次運(yùn)行run loop,你線程的run loop對(duì)會(huì)自動(dòng)處理之前未處理的消息,并通知相關(guān)的觀察者。具體的順序如下:
- 某一事件到達(dá)基于端口的源
- 定時(shí)器啟動(dòng)
- Run loop設(shè)置的時(shí)間已經(jīng)超時(shí)
- run loop被顯式喚醒
- 如果用戶定義的定時(shí)器啟動(dòng),處理定時(shí)器事件并重啟run loop。進(jìn)入步驟2
- 如果輸入源啟動(dòng),傳遞相應(yīng)的消息
- 如果run loop被顯式喚醒而且時(shí)間還沒超時(shí),重啟run loop。進(jìn)入步驟2
因?yàn)槎〞r(shí)器和輸入源的觀察者是在相應(yīng)的事件發(fā)生之前傳遞消息,所以通知的時(shí)間和實(shí)際事件發(fā)生的時(shí)間之間可能存在誤差。如果需要精確時(shí)間控制,你可以使用休眠和喚醒通知來幫助你校對(duì)實(shí)際發(fā)生事件的時(shí)間。
因?yàn)楫?dāng)你運(yùn)行run loop時(shí)定時(shí)器和其它周期性事件經(jīng)常需要被傳遞,撤銷run loop也會(huì)終止消息傳遞。典型的例子就是鼠標(biāo)路徑追蹤。因?yàn)槟愕拇a直接獲取到消息而不是經(jīng)由程序傳遞,因此活躍的定時(shí)器不會(huì)開始直到鼠標(biāo)追蹤結(jié)束并將控制權(quán)交給程序。
Run loop可以由run loop對(duì)象顯式喚醒。其它消息也可以喚醒run loop。例如,添加新的非基于端口的源會(huì)喚醒run loop從而可以立即處理輸入源而不需要等待其他事件發(fā)生后再處理。
從這個(gè)事件隊(duì)列中可以看出:
①如果是事件到達(dá),消息會(huì)被傳遞給相應(yīng)的處理程序來處理,?runloop處理完當(dāng)次事件后,run loop會(huì)退出,而不管之前預(yù)定的時(shí)間到了沒有。你可以重新啟動(dòng)run loop來等待下一事件。
②如果線程中有需要處理的源,但是響應(yīng)的事件沒有到來的時(shí)候,線程就會(huì)休眠等待相應(yīng)事件的發(fā)生。這就是為什么run loop可以做到讓線程有工作的時(shí)候忙于工作,而沒工作的時(shí)候處于休眠狀態(tài)。
?
2.4什么時(shí)候使用run loop
僅當(dāng)在為你的程序創(chuàng)建輔助線程的時(shí)候,你才需要顯式運(yùn)行一個(gè)run loop。Run loop是程序主線程基礎(chǔ)設(shè)施的關(guān)鍵部分。所以,Cocoa和Carbon程序提供了代碼運(yùn)行主程序的循環(huán)并自動(dòng)啟動(dòng)run loop。IOS程序中UIApplication的run方法(或Mac OS X中的NSApplication)作為程序啟動(dòng)步驟的一部分,它在程序正常啟動(dòng)的時(shí)候就會(huì)啟動(dòng)程序的主循環(huán)。類似的,RunApplicationEventLoop函數(shù)為Carbon程序啟動(dòng)主循環(huán)。如果你使用xcode提供的模板創(chuàng)建你的程序,那你永遠(yuǎn)不需要自己去顯式的調(diào)用這些例程。
對(duì)于輔助線程,你需要判斷一個(gè)run loop是否是必須的。如果是必須的,那么你要自己配置并啟動(dòng)它。你不需要在任何情況下都去啟動(dòng)一個(gè)線程的run loop。比如,你使用線程來處理一個(gè)預(yù)先定義的長(zhǎng)時(shí)間運(yùn)行的任務(wù)時(shí),你應(yīng)該避免啟動(dòng)run loop。Run loop在你要和線程有更多的交互時(shí)才需要,比如以下情況:
如果你決定在程序中使用run loop,那么它的配置和啟動(dòng)都很簡(jiǎn)單。和所有線程編程一樣,你需要計(jì)劃好在輔助線程退出線程的情形。讓線程自然退出往往比強(qiáng)制關(guān)閉它更好。
轉(zhuǎn)載于:https://www.cnblogs.com/crash-wu/p/5192626.html
總結(jié)
以上是生活随笔為你收集整理的Objective-C之run loop详解的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: vagrant --- vagrant部
- 下一篇: ios开发中的C语言学习—— 结构体简介