iOS之Run Loop詳解
來自: http://www.henishuo.com/ios-runloop-in-detail/
前言
做了一年多的IOS開發,對IOS和Objective-C深層次的了解還十分有限,大多還停留在會用API的級別,這是件挺可悲的事情。想學好一門語言還是需要深層次的了解它,這樣才能在使用的時候得心應手,出現各種怪異的問題時不至于不知所措。廢話少說,進入今天的正題。
不知道大家有沒有想過這個問題,一個應用開始運行以后放在那里,如果不對它進行任何操作,這個應用就像靜止了一樣,不會自發的有任何動作發生,但是如果我們點擊界面上的一個按鈕,這個時候就會有對應的按鈕響應事件發生。給我們的感覺就像應用一直處于隨時待命的狀態,在沒人操作的時候它一直在休息,在讓它干活的時候,它就能立刻響應。其實,這就是run loop的功勞。
本篇文章很有參考價值,因此轉載到本博客中,希望好文章對大家都有所幫助!
一、線程與run loop
1.1 線程任務的類型
再來說說線程。有些線程執行的任務是一條直線,起點到終點;而另一些線程要干的活則是一個圓,不斷循環,直到通過某種方式將它終止。直線線程如簡單的Hello World,運行打印完,它的生命周期便結束了,像曇花一現那樣;圓類型的如操作系統,一直運行直到你關機。在IOS中,圓型的線程就是通過run loop不停的循環實現的。
1.2 線程與run loop的關系
Run loop,正如其名,loop表示某種循環,和run放在一起就表示一直在運行著的循環。實際上,run loop和線程是緊密相連的,可以這樣說run loop是為了線程而生,沒有線程,它就沒有存在的必要。Run loops是線程的基礎架構部分,Cocoa和CoreFundation都提供了run loop對象方便配置和管理線程的run loop(以下都已Cocoa為例)。每個線程,包括程序的主線程(main thread)都有與之相應的run loop對象。
1.2.1 主線程的run loop默認是啟動的。
iOS的應用程序里面,程序啟動后會有一個如下的main()函數:
int main(int argc,char *argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([appDelegateclass])); } }
重點是UIApplicationMain()函數,這個方法會為main thread設置一個NSRunLoop對象,這就解釋了本文開始說的為什么我們的應用可以在無人操作的時候休息,需要讓它干活的時候又能立馬響應。
1.2.2 對其它線程來說,run loop默認是沒有啟動的
對其它線程來說,run loop默認是沒有啟動的,如果你需要更多的線程交互則可以手動配置和啟動,如果線程只是去執行一個長時間的已確定的任務則不需要。
1.2.3 獲取線程的run loop
在任何一個Cocoa程序的線程中,都可以通過:
NSRunLoop *runloop = [NSRunLoopcurrentRunLoop];
來獲取到當前線程的run loop。
1.3 關于run loop的幾點說明
1.3.1 Cocoa中的NSRunLoop類并不是線程安全的
我們不能再一個線程中去操作另外一個線程的run loop對象,那很可能會造成意想不到的后果。不過幸運的是CoreFundation中的不透明類CFRunLoopRef是線程安全的,而且兩種類型的run loop完全可以混合使用。Cocoa中的NSRunLoop類可以通過實例方法:
- (CFRunLoopRef)getCFRunLoop;
獲取對應的CFRunLoopRef類,來達到線程安全的目的。
1.3.2 Run loop的管理并不完全是自動的。
我們仍必須設計線程代碼以在適當的時候啟動run loop并正確響應輸入事件,當然前提是線程中需要用到run loop。而且,我們還需要使用while/for語句來驅動run loop能夠循環運行,下面的代碼就成功驅動了一個run loop:
BOOL isRunning = NO; do { isRunning = [[NSRunLoopcurrentRunLoop]runMode:NSDefaultRunLoopModebeforeDate:[NSDatedistantFuture]]; } while (isRunning);
1.3.3 Run loop同時也負責autorelease pool的創建和釋放
在使用手動的內存管理方式的項目中,會經常用到很多自動釋放的對象,如果這些對象不能夠被即時釋放掉,會造成內存占用量急劇增大。Run loop就為我們做了這樣的工作,每當一個運行循環結束的時候,它都會釋放一次autorelease pool,同時pool中的所有自動釋放類型變量都會被釋放掉。
1.3.4 Run loop的優點
一個run loop就是一個事件處理循環,用來不停的監聽和處理輸入事件并將其分配到對應的目標上進行處理。如果僅僅是想實現這個功能,你可能會想一個簡單的while循環不就可以實現了嗎,用得著費老大勁來做個那么復雜的機制?顯然,蘋果的架構設計師不是吃干飯的,你想到的他們早就想過了。
首先,NSRunLoop是一種更加高明的消息處理模式,他就高明在對消息處理過程進行了更好的抽象和封裝,這樣才能是的你不用處理一些很瑣碎很低層次的具體消息的處理,在NSRunLoop中每一個消息就被打包在input source或者是timer source(見后文)中了。
其次,也是很重要的一點,使用run loop可以使你的線程在有工作的時候工作,沒有工作的時候休眠,這可以大大節省系統資源。
二、Run loop相關知識點
2.1輸入事件來源
Run loop接收輸入事件來自兩種不同的來源:輸入源(input source)和定時源(timer source)。兩種源都使用程序的某一特定的處理例程來處理到達的事件。圖-1顯示了run loop的概念結構以及各種源。
需要說明的是,當你創建輸入源,你需要將其分配給run loop中的一個或多個模式(什么是模式,下文將會講到)。模式只會在特定事件影響監聽的源。大多數情況下,run loop運行在默認模式下,但是你也可以使其運行在自定義模式。若某一源在當前模式下不被監聽,那么任何其生成的消息只在run loop運行在其關聯的模式下才會被傳遞。
圖-1 Runloop的結構和輸入源類型
2.1.1輸入源(input source)
傳遞異步事件,通常消息來自于其他線程或程序。輸入源傳遞異步消息給相應的處理例程,并調用runUntilDate:方法來退出(在線程里面相關的NSRunLoop對象調用)。
2.1.1.1基于端口的輸入源
基于端口的輸入源由內核自動發送。
Cocoa和Core Foundation內置支持使用端口相關的對象和函數來創建的基于端口的源。例如,在Cocoa里面你從來不需要直接創建輸入源。你只要簡單的創建端口對象,并使用NSPort的方法把該端口添加到run loop。端口對象會自己處理創建和配置輸入源。
在Core Foundation,你必須人工創建端口和它的run loop源。我們可以使用端口相關的函數(CFMachPortRef,CFMessagePortRef,CFSocketRef)來創建合適的對象。下面的例子展示了如何創建一個基于端口的輸入源,將其添加到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(); [poolrelease]; } CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source,kCFRunLoopDefaultMode); CFRelease(source); }
2.1.1.2自定義輸入源
自定義的輸入源需要人工從其他線程發送。
為了創建自定義輸入源,必須使用Core Foundation里面的CFRunLoopSourceRef類型相關的函數來創建。你可以使用回調函數來配置自定義輸入源。Core Fundation會在配置源的不同地方調用回調函數,處理輸入事件,在源從run loop移除的時候清理它。
除了定義在事件到達時自定義輸入源的行為,你也必須定義消息傳遞機制。源的這部分運行在單獨的線程里面,并負責在數據等待處理的時候傳遞數據給源并通知它處理數據。消息傳遞機制的定義取決于你,但最好不要過于復雜。創建并啟動自定義輸入源的示例如下:
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(); [poolrelease]; } CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source,kCFRunLoopDefaultMode); CFRelease(source); }
2.1.1.3 Cocoa上的Selector源
除了基于端口的源,Cocoa定義了自定義輸入源,允許你在任何線程執行selector方法。和基于端口的源一樣,執行selector請求會在目標線程上序列化,減緩許多在線程上允許多個方法容易引起的同步問題。不像基于端口的源,一個selector執行完后會自動從run loop里面移除。
當在其他線程上面執行selector時,目標線程須有一個活動的run loop。對于你創建的線程,這意味著線程在你顯式的啟動run loop之前是不會執行selector方法的,而是一直處于休眠狀態。
NSObject類提供了類似如下的selector方法:
- (void)performSelectorOnMainThread:(SEL)aSelectorwithObject:(id)argwaitUntilDone:(BOOL)waitmodes:(NSArray *)array;
2.1.2 定時源(timer source)
定時源在預設的時間點同步方式傳遞消息,這些消息都會發生在特定時間或者重復的時間間隔。定時源則直接傳遞消息給處理例程,不會立即退出run loop。
需要注意的是,盡管定時器可以產生基于時間的通知,但它并不是實時機制。和輸入源一樣,定時器也和你的run loop的特定模式相關。如果定時器所在的模式當前未被run loop監視,那么定時器將不會開始直到run loop運行在相應的模式下。類似的,如果定時器在run loop處理某一事件期間開始,定時器會一直等待直到下次run loop開始相應的處理程序。如果run loop不再運行,那定時器也將永遠不啟動。
創建定時器源有兩種方法,
方法一:
NSTimer *timer = [NSTimerscheduledTimerWithTimeInterval:4.0 target:self selector:@selector(backgroundThreadFire:) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop]addTimer:timerforMode:NSDefaultRunLoopMode];
方法二:
[NSTimerscheduledTimerWithTimeInterval:10 target:self selector:@selector(backgroundThreadFire:) userInfo:nil repeats:YES];
2.2 RunLoop觀察者
源是在合適的同步或異步事件發生時觸發,而run loop觀察者則是在run loop本身運行的特定時候觸發。你可以使用run loop觀察者來為處理某一特定事件或是進入休眠的線程做準備。你可以將run loop觀察者和以下事件關聯:
- Runloop入口
- Runloop何時處理一個定時器
- Runloop何時處理一個輸入源
- Runloop何時進入睡眠狀態
- Runloop何時被喚醒,但在喚醒之前要處理的事件
- Runloop終止
和定時器類似,在創建的時候你可以指定run loop觀察者可以只用一次或循環使用。若只用一次,那么在它啟動后,會把它自己從run loop里面移除,而循環的觀察者則不會。定義觀察者并把它添加到run loop,只能使用Core Fundation。下面的例子演示了如何創建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表示選擇監聽定時器觸發前處理事件,后面的YES表示循環監聽。
2.3 RunLoop的事件隊列
每次運行run loop,你線程的run loop對會自動處理之前未處理的消息,并通知相關的觀察者。具體的順序如下:
- 通知觀察者run loop已經啟動
- 通知觀察者任何即將要開始的定時器
- 通知觀察者任何即將啟動的非基于端口的源
- 啟動任何準備好的非基于端口的源
- 如果基于端口的源準備好并處于等待狀態,立即啟動;并進入步驟9。
- 通知觀察者線程進入休眠
- 將線程置于休眠直到任一下面的事件發生:
- 某一事件到達基于端口的源
- 定時器啟動
- Run loop設置的時間已經超時
- run loop被顯式喚醒
- 通知觀察者線程將被喚醒。
- 處理未處理的事件
- 如果用戶定義的定時器啟動,處理定時器事件并重啟run loop。進入步驟2
- 如果輸入源啟動,傳遞相應的消息
- 如果run loop被顯式喚醒而且時間還沒超時,重啟run loop。進入步驟2
- 通知觀察者run loop結束。
因為定時器和輸入源的觀察者是在相應的事件發生之前傳遞消息,所以通知的時間和實際事件發生的時間之間可能存在誤差。如果需要精確時間控制,你可以使用休眠和喚醒通知來幫助你校對實際發生事件的時間。
因為當你運行run loop時定時器和其它周期性事件經常需要被傳遞,撤銷run loop也會終止消息傳遞。典型的例子就是鼠標路徑追蹤。因為你的代碼直接獲取到消息而不是經由程序傳遞,因此活躍的定時器不會開始直到鼠標追蹤結束并將控制權交給程序。
Run loop可以由run loop對象顯式喚醒。其它消息也可以喚醒run loop。例如,添加新的非基于端口的源會喚醒run loop從而可以立即處理輸入源而不需要等待其他事件發生后再處理。
從這個事件隊列中可以看出:
- ①如果是事件到達,消息會被傳遞給相應的處理程序來處理, runloop處理完當次事件后,run loop會退出,而不管之前預定的時間到了沒有。你可以重新啟動run loop來等待下一事件。
- ②如果線程中有需要處理的源,但是響應的事件沒有到來的時候,線程就會休眠等待相應事件的發生。這就是為什么run loop可以做到讓線程有工作的時候忙于工作,而沒工作的時候處于休眠狀態。
2.4 什么時候使用run loop
僅當在為你的程序創建輔助線程的時候,你才需要顯式運行一個run loop。Run loop是程序主線程基礎設施的關鍵部分。所以,Cocoa和Carbon程序提供了代碼運行主程序的循環并自動啟動run loop。IOS程序中UIApplication的run方法(或Mac OS X中的NSApplication)作為程序啟動步驟的一部分,它在程序正常啟動的時候就會啟動程序的主循環。類似的,RunApplicationEventLoop函數為Carbon程序啟動主循環。如果你使用xcode提供的模板創建你的程序,那你永遠不需要自己去顯式的調用這些例程。
對于輔助線程,你需要判斷一個run loop是否是必須的。如果是必須的,那么你要自己配置并啟動它。你不需要在任何情況下都去啟動一個線程的run loop。比如,你使用線程來處理一個預先定義的長時間運行的任務時,你應該避免啟動run loop。Run loop在你要和線程有更多的交互時才需要,比如以下情況:
- 使用端口或自定義輸入源來和其他線程通信
- 使用線程的定時器
- Cocoa中使用任何performSelector…的方法
- 使線程周期性工作
如果你決定在程序中使用run loop,那么它的配置和啟動都很簡單。和所有線程編程一樣,你需要計劃好在輔助線程退出線程的情形。讓線程自然退出往往比強制關閉它更好。
關注我
關注 | ||
---|---|---|
Swift/ObjC技術群一 | ||
Swift/ObjC技術群二 | 494669518 | 群二若已滿,請申請群三 |
Swift/ObjC技術群三 | 461252383 | 群三若已滿,會有提示信息 |
關注微信公眾號 | iOSDevShares | 關注微信公眾號,會定期地推送好文章 |
關注新浪微博賬號 | 標哥Jacky | 關注微博,每次發布文章都會分享到新浪微博,即可時時閱讀文章 |
關注標哥的GitHub | CoderJackyHuang | 這里有很多的Demo和開源組件,大家可以關注哦! |
關于我 | 進一步了解標哥 | 大家若對筆者感興趣,可以關注我哦!如果覺得文章對您很有幫助,可捐助我! |