NSRunLoop原理詳解——再也不有盲點

編程最怕的就是有盲點,不肯定,而runloop官網對其說起的又不多;那麼看完這篇應該使你有底氣不少~編程

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

RunLoop消息類型(事件源)

一句話歸納:很複雜,各類各樣 :)spa

事件源

不過,根據上圖咱們能夠將消息分爲二種類型,第一種類型又能夠細分爲三種,此三種共同點就是它們都是異步執行的

  • Port:

監聽程序的Mach ports,Mach ports是一個比較底層的東西,能夠簡單的理解爲:內核經過port這種方式將信息發送,而mach則監聽內核發來的port信息,而後將其整理,打包發給runloop。

  • Customer:

很明顯,由開發人員本身發送。不只僅是發送,過程的話至關複雜,蘋果也提供了一個CFRunLoopSource來幫助處理。因爲不多用到,能夠簡單說下核心,可是對幫助咱們理解runloop卻頗有幫助:

  1. 定義輸入源(數據結構)
  2. 將輸入源添加到runloop,那麼這樣就有了接受者,即爲R1
  3. 協調輸入源的客戶端(單獨線程),專門監聽消息,而後將消息打包成runloop可以處理的樣式,即第一步定義的輸入源。它相似Mach的功能
  4. 誰來發送消息的問題?上面的machport是由內核發送的。自定義的固然要咱們本身發送了。。。首先必須是另外一個線程來發送(固然若是隻是測試的話能夠和第三步在同一個線程),先發送消息給輸入源,而後喚醒R1,由於R1通常處於休眠狀態,而後R1根據輸入源來作相應的處理
  • Selector Sources:

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:它的事件發送是同步的,這個用的比較多,會在下一篇專門介紹

  • Observers,觀察者:首先它並不屬於事件源(不會影響runloop的生命週期),它比較特殊,用於觀察runloop自身的一些狀態的,有如下幾種:
    1. 進入runloop
    2. runloop即將執行定時器
    3. runloop即將執行輸入源(Port,Customer,Selector Sources)
    4. runloop即將休眠
    5. runloop被喚醒,在處理完喚醒它的事件以前
    6. 退出

下面舉例,監聽全部狀態,在非主線程(能夠看到一個完整的週期):

+ (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---");
}

打印結果:一個完整的週期

runloopObserver

RunLoop模式

runloop的模式,使得runloop顯得更加靈活,適應更多的應用場景。

上面提到的事件源,都是處於特定的模式下的,若是和當前runloop的模式不一致則不會獲得響應,舉個例子:

若是定時器處於mode1,而runloop運行在mode2,則定時器不會觸發,只有runloop運行在mode1時,定時器纔會觸發。

系統爲咱們提供了多種模式,下面列一些比較常遇到的:

  • kCFRunLoopDefaultMode: App的默認 Mode,一般主線程是在這個 Mode 下運行的。
  • UITrackingRunLoopMode: 界面跟蹤 Mode,用於 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其餘 Mode 影響。
  • UIInitializationRunLoopMode: 在剛啓動 App 時第進入的第一個 Mode,啓動完成後就再也不使用。
  • NSRunLoopCommonModes: 包含了多種模式:default, modal, 和tracking modes。

除了系統給咱們的模式,咱們本身也能夠自定義。

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生命週期

能夠分爲三步:建立->運行(開啓,內部循環)->退出

1. runloop建立

蘋果是不容許開發人員手動建立runloop,runloop是伴隨着線程的建立而建立,線程與runloop是一一對應的,具備惟一性,另外建立還區分是否爲主線程

  • 主線程:系統會自動建立

  • 非主線程:系統不會自動建立,開發人員必須顯示的調用[NSRunLoop currentRunLoop]方法來獲取runloop的時候,系統纔會建立,相似懶加載

系統只提供了兩種方法獲取runloop,currentRunLoopmainRunLoop,能夠看出非主線程只有在本身的線程內才能得到runloop。

2. 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自己的狀態而已。

  • 內部循環(略複雜)

內部循化

這樣看起來仍是比較清晰的。

關於自動釋放池提一下(下一篇會作詳細說明):

  • 第1步的觀察者(優先級較高)會建立自動釋放池
  • 第6步的觀察者,會銷燬老的自動釋放池,並建立新的自動釋放池,對於一個runloop來講,此步驟會不斷的循環
  • 第10步的觀察者,銷燬自動釋放池

上面提到的自動釋放池的處理固然是系統幫咱們處理的,非主線程和主線程系統都幫咱們作了處理。官方說到,若是你使用POSIX thread APIs建立線程,那就是另一套內存回收系統了,是不會用autoreleasePool,系統固然也不會建立。

3. runloop退出

能夠用如下方式退出runloop

  • 設置最大時間到期:推薦使用這種方式
  • modeItem(事件源)爲空:但並不推薦這樣退出,由於一些系統的Item咱們並不知道
  • 調用CFRunLoopStop,退出runloop並將程序控制權交給調用者(若是runloop有嵌套,則只退出最內層runloop),一些狀況下,CFRunLoopStop並不能真正的退出runloop,好比你使用下面的2種方法開啓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退出後,是不會被釋放的(或者說當即),它大概極可能是伴隨着線程的釋放而釋放。。。。。。歡迎補充

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]);
}

打印結果

runloopNest

例程中外層runloop運行在NSDefaultRunLoopMode模式下,而後在它的callout函數(定時器1)又執行runloop,運行在UITrackingRunLoopMode模式下,實現嵌套,而後在內層runloop的callout(timerHandle2),中止運行當前runloop,即中止內層runloop,這時又回到外層循環。外層runloop只運行2秒到期。-end-

上面嵌套是運行在不一樣模式下,當同一模式下的runloop出現嵌套時,蘋果依然處理的很好。舉例:

  1. 將t1(timer1)添加到r1(runloop1),並在NSDefaultRunLoopMode模式下運行
  2. 在t1的響應函數裏,將t2添加到r2,r2在NSDefaultRunLoopMode模式下運行
  3. 此時很明顯,r2處於嵌套內層,則只應該運行t2的響應函數
  4. 在t2的響應函數裏,退出r2,此時回到r1
  5. 會運行t1與t2的響應函數

可能你會以爲很詫異,t2怎麼也會運行呢????其實這很符合邏輯:
假設在第2步驟中,咱們沒有執行r2,即沒有r2,那麼t2仍是加到了r1上。既然是加到了r1那執行就不難理解了。(是否感受蘋果很強大?)

注意:r1與r2表明的是同一runloop,只是調用棧不一樣,或者說嵌套層。若是把runloop理解爲一個函數,那麼就能夠理解爲函數r1調用了自身,那個"自身"稱爲r2。

參考:http://www.jianshu.com/p/4263188ed940

相關文章
相關標籤/搜索