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


編程最怕的就是有盲點,不確定,而runloop官網對其提及的又很少;那么看完這篇應該使你有底氣很多~

RunLoop整體介紹

An event-processing loop, during which events are received and dispatched to appropriate handlers.

事件運行循環:就類似下面的while循環部分,當然要復雜很多,可以把它抽象成如下代碼:

main() {
    initialize();
    do {
        message = get_next_message();
        process_message(message);
    } while (message != quit);
}

“消息”循環,等待消息(會休眠)->接收消息->處理消息。通過上面的代碼,runloop本質就是提供了一種消息處理模式,只不過它封裝抽象的太好了(一般開發的時候根本就感覺不到,或者說不用關心)。

runloop相當於幫我們打包了各種消息,並將消息發送給指定的接受者。

可以將runloop理解為一個函數,功能是一個消息循環,有消息則處理,沒有消息則休眠。(注意:runloop實質是一個對象,但是不影響以上的假設)

簡單使用:新建一個線程,添加一個定時器,然后運行即可

- (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很相似?

RunLoop消息類型(事件源)

一句話概括:很復雜,各種各樣 :)

事件源

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

  • 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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM