編程最怕的就是有盲點,不肯定,而runloop官網對其說起的又不多;那麼看完這篇應該使你有底氣不少~編程
An event-processing loop, during which events are received and dispatched to appropriate handlers.數據結構
事件運行循環:就相似下面的while循環部分,固然要複雜不少,能夠把它抽象成以下代碼:app
main() { initialize(); do { message = get_next_message(); process_message(message); } while (message != quit); }
「消息」循環,等待消息(會休眠)->接收消息->處理消息。經過上面的代碼,runloop本質就是提供了一種消息處理模式,只不過它封裝抽象的太好了(通常開發的時候根本就感受不到,或者說不用關心)。異步
runloop至關於幫咱們打包了各類消息,並將消息發送給指定的接受者。async
能夠將runloop理解爲一個函數,功能是一個消息循環,有消息則處理,沒有消息則休眠。(注意:runloop實質是一個對象,可是不影響以上的假設)函數
簡單使用:新建一個線程,添加一個定時器,而後運行便可oop
- (void)timerFire { NSLog(@"mode:%@",[[NSRunLoop currentRunLoop] currentMode]); } - (void)runLoopTest { dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSTimer *tickTimer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:2 target:self selector:@selector(modeTestTimer) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:timerFire forMode:NSDefaultRunLoopMode]; [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; }); }
若是你接觸過嵌入式操做系統(純內核)開發,那麼對下面代碼確定很熟悉測試
void ledTask (void *p_arg) { initialize(); while (1) { LED_ON(); delay_ms(500); LED_OFF(); delay_ms(500); }; }
LED閃爍線程,讓一個LED燈1HZ的頻率閃爍,功能很簡單:首先初始化,而後進入while(1)死循環,延遲函數會使線程進入休眠(節省CPU)。直到程序死掉線程結束。是否和runloop很類似?ui
一句話歸納:很複雜,各類各樣 :)spa
不過,根據上圖咱們能夠將消息分爲二種類型,第一種類型又能夠細分爲三種,此三種共同點就是它們都是異步執行的
監聽程序的Mach ports,Mach ports是一個比較底層的東西,能夠簡單的理解爲:內核經過port這種方式將信息發送,而mach則監聽內核發來的port信息,而後將其整理,打包發給runloop。
很明顯,由開發人員本身發送。不只僅是發送,過程的話至關複雜,蘋果也提供了一個CFRunLoopSource來幫助處理。因爲不多用到,能夠簡單說下核心,可是對幫助咱們理解runloop卻頗有幫助:
NSObject類提供了不少方法供咱們使用,這些方法是添加到runloop的,因此若是沒有開啓runloop的話,不會運行(不過有個坑,請看下面介紹)。
/// 主線程 performSelectorOnMainThread:withObject:waitUntilDone: performSelectorOnMainThread:withObject:waitUntilDone:modes: /// 指定線程 performSelector:onThread:withObject:waitUntilDone: performSelector:onThread:withObject:waitUntilDone:modes: /// 針對當前線程 performSelector:withObject:afterDelay: performSelector:withObject:afterDelay:inModes: /// 取消,在當前線程,和上面兩個方法對應 cancelPreviousPerformRequestsWithTarget: cancelPreviousPerformRequestsWithTarget:selector:object:
下面提供的方法是在指定的線程運行aSelector
,通常狀況下aSelector
會添加到指定線程的runloop。但,若是調用線程和指定線程爲同一線程,且wait
參數設爲YES,那麼aSelector
會直接在指定線程運行,再也不添加到runloop。
performSelectorOnMainThread:withObject:waitUntilDone: performSelectorOnMainThread:withObject:waitUntilDone:modes: performSelector:onThread:withObject:waitUntilDone: performSelector:onThread:withObject:waitUntilDone:modes:
其實這也很好理解,假設這種狀況也添加到指定線程的runloop,咱們能夠這樣反向理解:1,當前線程runloop尚未開啓,那麼aSelector
就不會被執行,然而你卻一直在等待,形成線程卡死。2,當前線程runloop已經開啓,那麼調用performSelector
這個方法的位置確定是處於runloop的callout方法裏面,在這裏等待runloop再callout從而調用aSelector
方法完成,顯然也是死等待,線程卡死。。。
還有一些performSelector
方法,是不會添加到runloop的,而是直接執行,能夠按照上面的特殊狀況進行理解。方法列舉以下:
- (id)performSelector:(SEL)aSelector; - (id)performSelector:(SEL)aSelector withObject:(id)object; - (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
看到這裏,是否感受有些亂???只要記住沒有延遲或者等待的都不會添加到runloop,有延遲或者等待的還有排除上面提到的特殊狀況。
Timer Sources:它的事件發送是同步的,這個用的比較多,會在下一篇專門介紹
下面舉例,監聽全部狀態,在非主線程(能夠看到一個完整的週期):
+ (void)observerTest { dispatch_async(dispatch_get_global_queue(0, 0), ^{ /** param1: 給observer分配存儲空間 param2: 須要監聽的狀態類型:kCFRunLoopAllActivities監聽全部狀態 param3: 是否每次都須要監聽,若是NO則一次以後就被銷燬,再也不監聽,相似定時器的是否重複 param4: 監聽的優先級,通常傳0 param5: 監聽到的狀態改變以後的回調 return: 觀察對象 */ CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { switch (activity) { case kCFRunLoopEntry: NSLog(@"即將進入runloop"); break; case kCFRunLoopBeforeTimers: NSLog(@"即將處理timer"); break; case kCFRunLoopBeforeSources: NSLog(@"即將處理input Sources"); break; case kCFRunLoopBeforeWaiting: NSLog(@"即將睡眠"); break; case kCFRunLoopAfterWaiting: NSLog(@"從睡眠中喚醒,處理完喚醒源以前"); break; case kCFRunLoopExit: NSLog(@"退出"); break; default: break; } }); // 沒有任何事件源則不會進入runloop [NSTimer scheduledTimerWithTimeInterval:3 target:self selector:@selector(doFireTimer) userInfo:nil repeats:NO]; CFRunLoopAddObserver([[NSRunLoop currentRunLoop] getCFRunLoop], observer, kCFRunLoopDefaultMode); [[NSRunLoop currentRunLoop] run]; }); } + (void)doFireTimer { NSLog(@"---fire---"); }
打印結果:一個完整的週期
runloop的模式,使得runloop顯得更加靈活,適應更多的應用場景。
上面提到的事件源,都是處於特定的模式下的,若是和當前runloop的模式不一致則不會獲得響應,舉個例子:
若是定時器處於mode1,而runloop運行在mode2,則定時器不會觸發,只有runloop運行在mode1時,定時器纔會觸發。
系統爲咱們提供了多種模式,下面列一些比較常遇到的:
除了系統給咱們的模式,咱們本身也能夠自定義。
NSRunLoopMode
的類型爲字符串類型,定義:typedef NSString * NSRunLoopMode
,自定義類型就很簡單了,示例代碼以下:直接調用runLoopModeTest方法便可測試
- (void)modeTestTimer { NSLog(@"mode:%@",[[NSRunLoop currentRunLoop] currentMode]); } /// 這裏使用非主線程,主要考慮若是一直處於customMode模式,則主線癱瘓 - (void)runLoopModeTest { dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSTimer *tickTimer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:2 target:self selector:@selector(modeTestTimer) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:tickTimer forMode:@"customMode"]; [[NSRunLoop currentRunLoop] runMode:@"customMode" beforeDate:[NSDate distantFuture]]; }); }
runloop模式的切換
主線程沒有使用runloop嵌套是根據個人測試得出,沒辦法,官方文檔太太太少,也沒有更底層源碼,只有CFRunLoop的源碼:http://opensource.apple.com/tarballs/CF/CF-855.17.tar.gz。
最後總結下,thread--runloop--mode--event sources,關係能夠表示以下:
能夠分爲三步:建立->運行(開啓,內部循環)->退出
蘋果是不容許開發人員手動建立runloop,runloop是伴隨着線程的建立而建立,線程與runloop是一一對應的,具備惟一性,另外建立還區分是否爲主線程
主線程:系統會自動建立
非主線程:系統不會自動建立,開發人員必須顯示的調用[NSRunLoop currentRunLoop]
方法來獲取runloop的時候,系統纔會建立,相似懶加載
系統只提供了兩種方法獲取runloop,currentRunLoop
和mainRunLoop
,能夠看出非主線程只有在本身的線程內才能得到runloop。
NSRunLoop提供的方法: - (void)run; // 默認模式 - (void)runUntilDate:(NSDate *)limitDate; - (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;
CFRunLoop提供的函數: /// 默認模式 void CFRunLoopRun(void); /// 在指定模式,指定時間,運行 CFRunLoopRunResult CFRunLoopRunInMode(CFRunLoopMode mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled);
當執行了上面的運行方法後,若是runloop所在的模式沒有對應的事件源,即上面圖中提到的input sources、timer sources,會直接退出當前runloop(注意:是當前)。另外注意的是,input sources裏面的Selector Sources,它有一些特殊狀況,上面也提到了。這些狀況下runloop仍是會直接退出。
網上有不少說到事件源包括了observe,實際上是不包含的,即runloop是否退出與observe沒有關係,observe只是監聽runloop自己的狀態而已。
這樣看起來仍是比較清晰的。
關於自動釋放池提一下(下一篇會作詳細說明):
上面提到的自動釋放池的處理固然是系統幫咱們處理的,非主線程和主線程系統都幫咱們作了處理。官方說到,若是你使用POSIX thread APIs建立線程,那就是另一套內存回收系統了,是不會用autoreleasePool,系統固然也不會建立。
能夠用如下方式退出runloop
- (void)run; // 默認模式 - (void)runUntilDate:(NSDate *)limitDate;
當執行NSRunLoop的run方法,一旦成功(默認模式下有事件源),那麼run會不停的調用runMode:beforeDate:來運行runloop,那麼即使CFRunLoopStop退出了一個runloop,很快會有另外一個runloop執行。即:若是你想退出一個runloop,那麼你就不應調用run方法來開啓runloop
runUntilDate:與run同樣不停的執行runMode:beforeDate:方法,CFRunLoopStop也是退不出來的,不一樣的是runUntilDate:本身有個期限,超過這個期限會自動退出
很明顯,你會想到利用事件源爲空來退出,這種方法上面已經說了,不推薦。。。
一個不想回答的問題:runloop自己的釋放。有人會糾結這個問題,通過多方查問、資料、源碼、測試加自身理解,得出:runloop退出後,是不會被釋放的(或者說當即),它大概極可能是伴隨着線程的釋放而釋放。。。。。。歡迎補充
嵌套,剛接觸時感受很神奇,然而一入嵌套深似海。。。特別是約瑟夫環的問題(http://www.jianshu.com/p/3c62ac7d9285)。。。
在當前runloop的callout函數裏面執行上runloop,例程代碼以下:
/** runloop嵌套測試, */ + (void)nestTest { dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSTimer *tickTimer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(timerHandle1) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:tickTimer forMode:NSDefaultRunLoopMode]; [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:2]]; NSLog(@"-end-"); }); } /** 不停的運行與退出最內層runloop */ + (void)timerHandle1 { NSLog(@"timer111-%@",[[NSRunLoop currentRunLoop] currentMode]); // 防止屢次添加timer,開發中應特別注意 static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSTimer *tickTimer2 = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(timerHandle2) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:tickTimer2 forMode:UITrackingRunLoopMode]; }); [[NSRunLoop currentRunLoop] runMode:UITrackingRunLoopMode beforeDate:[NSDate distantFuture]]; } + (void)timerHandle2 { NSLog(@"timer222-%@",[[NSRunLoop currentRunLoop] currentMode]); CFRunLoopStop([[NSRunLoop currentRunLoop] getCFRunLoop]); }
打印結果
例程中外層runloop運行在NSDefaultRunLoopMode
模式下,而後在它的callout函數(定時器1)又執行runloop,運行在UITrackingRunLoopMode
模式下,實現嵌套,而後在內層runloop的callout(timerHandle2),中止運行當前runloop,即中止內層runloop,這時又回到外層循環。外層runloop只運行2秒到期。-end-
上面嵌套是運行在不一樣模式下,當同一模式下的runloop出現嵌套時,蘋果依然處理的很好。舉例:
NSDefaultRunLoopMode
模式下運行NSDefaultRunLoopMode
模式下運行可能你會以爲很詫異,t2怎麼也會運行呢????其實這很符合邏輯:
假設在第2步驟中,咱們沒有執行r2,即沒有r2,那麼t2仍是加到了r1上。既然是加到了r1那執行就不難理解了。(是否感受蘋果很強大?)
注意:r1與r2表明的是同一runloop,只是調用棧不一樣,或者說嵌套層。若是把runloop理解爲一個函數,那麼就能夠理解爲函數r1調用了自身,那個"自身"稱爲r2。