iOS-NSRunLoop詳解+++

概念

Runloop就像它的名字一樣,就是跑環.我的理解就是一個死循環.是一個可以隨時睡眠,隨時喚醒的死循環

大家可以想一下,手機app爲什麼會一直運行?而且在接收到用戶點擊等等操作時就會有所反映.這個離不開runloop.

iOS app啓動時就會啓動一個runloop,而且這種模式應該Android也有,所以纔會有了app能一直運行

每個線程都有一個runloop,但是隻有主線程的runloop是默認開啓的,其他子線程需要調用NSRunLoop *runloop = [NSRunLoop currentRunLoop];獲取runloop的同時就會創建runloop

一個線程可以創建多個runloop,但是隻能是嵌套模式.也就是一個線程只有一個根runloop

作用


  1. 使程序一直運行,並且接收用戶輸入等事件

  2. 決定程序什麼時候處理什麼事件

  3. 調用方面 解耦(比如用戶劃一下屏幕,會產生N個event事件,但是用戶不可能等着被調方全部執行完再進行下一步的動作,也就是會將此係列事件扔到一個消息隊列裏,每次再從消息隊列裏面取,主調方與被調方實現解耦)

  4. 節省CPU(因爲runloop在沒事幹的時候是休眠狀態,只有接收到信號的時候纔會喚醒,執行相應的操作)


runloop是由事件驅動的

這裏區分下命令式驅動跟事件驅動

命令式驅動

1
2
3
4
int main(int argc, charchar * argv[]) {  
    NSLog(@"hello world");  
    return 0;  
}

event驅動(僞代碼)

1
2
3
4
5
6
7
8
int main(int argc, charchar * argv[]) {  
    while (AppIsRunning) {  
        id whoWakesMe = SleepForWakingUp;  
        id event = GetEvent(whoWakesMe);  
        HandleEvent(event);  
    }  
    return 0;  
}

舉個栗子,就像一個人活着就是個大runloop

1
2
3
4
5
6
7
8
while (活着){  
    有事幹了 = 我睡覺了沒事別叫我();  
    if(該喫飯){  
        喫飯();  
    }else if(該上廁所){  
        上廁所();  
    }  
}

NSRunloop是對CFRunloop的封裝

與CFRunloop相關的有GCD,mach kernel是蘋果內核的東西,還有block,pthread等


與咱們平時敲代碼比較近一層有以下這些


  • NSTimer 計時器完全依賴於runloop

  • UIEvent 事件的產生到分發給代碼都是通過runloop

  • Autorelease 自動釋放也是在runloop跑完一圈後

  • NSObject(NSDelayedPerforming) performSelector,cancel

  • NSObject(NSThreadPerformAddition) performSelectorOnMainThread,performSelectorOnBackgroundThread

  • CA層的CADisplayLink(每畫一幀會有一個回調),CATransition,CAAnimation

  • dispatch_get_main_queue()

  • NSURLConnection

  • AFNetworking,它的delegate跟網絡傳輸數據都是在它的runloop裏面執行的

  • NSPort 描述通訊信道的抽象類

  • 等等..

如圖所示:APP啓動,start-->main.m進入-->Graphics Services(處理硬件交互的服務,比如用戶點擊屏幕)-->RunLoop(CFRunLoop開頭的)-->Handle event


在runloop中定義了以下6種函數

1
2
3
4
5
6
static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();  
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();  
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();  
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();  
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();  
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();

幾乎所有的函數都是從以上6中函數中調起.比如上圖中就是調用的static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__()
然後開始調用event

RunLoop機制


  • RunLoop跟Thread是一一綁定的(也就是之前說的一個Thread裏只有一個根runloop但是可以嵌套N個)

  • CFRunLoopMode:RunLoop必須在系統定義的幾種模式下運行

  • 下邊幾種是在RunLoopMode裏面的

比較抽象,繼續往下走

CFRunLoopTimer包括以下幾種常見方法的封裝


CFRunLoopSource

  • source是RunLoop的數據源(輸入源)的抽象類(protocol)

  • RunLoop定義了兩個version的Source:

1.source0:處理App內部事件,App自己負責管理(出發),如UIEvent、CFSocket

2.source1:由RunLoop和內核管理,Mach Port(進程間通訊端口)驅動,如CFMachPort、CFMessagePort

  • 如果有需要,可從中選擇一種實現自己的source(基本不會發生)

CFRunLoopObserver:告知外界當前狀態

1
2
3
4
5
6
7
kCFRunLoopEntry = (1UL << 0),// 即將進入Loop  
kCFRunLoopBeforeTimers = (1UL << 1),// 即將處理 Timer  
kCFRunLoopBeforeSources = (1UL << 2),// 即將處理 Source  
kCFRunLoopBeforeWaiting = (1UL << 5),// 即將進入休眠  
kCFRunLoopAfterWaiting = (1UL << 6),// 剛從休眠中喚醒  
kCFRunLoopExit = (1UL << 7),// 即將退出Loop  
kCFRunLoopAllActivities = 0x0FFFFFFFU//所有狀態

RunLoopObserver與Autorelease Pool

大家面試的時候可以問問面試者這個問題,autorelease的對象到底在什麼時候釋放?


根據孫源大神測試,AutoreleasePool通常在RunLoop兩次Sleep之間釋放

CFRunLoopMode

  • RunLoop在同一時間只能且必須在一種特定的Mode下Run

  • 更換Mode時,需要停止當前RunLoop,然後重啓新的RunLoop

  • Mode是ios App流暢滑動的關鍵(因爲在滑動時的Mode跟平時運行的Mode是不一樣,從而避免干擾)

  • 也可以基於系統的Mode創建自己的Mode(也是基本不會發生的)

系統定義的Mode有以下幾種:

  1. CFRunLoopDefaultMode: 這個是默認 Mode,也是空閒狀態。主線程通常在這個 Mode 下運行的。

  2. UITrackingRunLoopMode: ScrollView滾動時候的模式。

  3. UIInitializationRunLoopMode: 在剛啓動程序時進入的第一個 Mode,私有,啓動完成後就不再使用。

  4. GSEventReceiveRunLoopMode: 接受系統事件的內部的Mode,這個Mode由GraphicsServices調用在CFRunLoopRunSpecific前面。通常用不到。

  5. CFRunLoopCommonModes: 這是一個數組,默認包括了第1和第2種模式,可以添加自己的Mode。

UITrackingRunLoopMode與NSTimer

下面的方法Timer被添加到NSDefaultRunLoopMode,在滑動Scrollview的時候系統會切換至UITrackingRunLoopMode,Timer就會暫時停止

1
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTick:) userInfo:nil repeats:YES];

若不希望Timer被滑動影響,需添加到NSRunLoopCommonMode

1
2
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTick:) userInfo:nil repeats:YES];  
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];

下圖表示App在滑動時的Mode切換



RunLoop與dispatch_get_main_queue()

前面有說到GCD跟RunLoop有關係,其實本身GCD跟RunLoop是沒有關係的,但是如果把queue填成main_queue就有關係了,關係只在於調起的過程是在RunLoop

GCD的主線程就是App的主線程,所以在GCD牽扯的主線程會轉交給RunLoop去調起

RunLoop的掛起與喚醒

在App運行時,在Debug欄裏按下暫停,會出現以下堆棧

這就是RunLoop的睡眠狀態,與剛剛說的MachPort有關係,圖片裏面上邊的兩個mach_msg會指定一個端口發給內核一個消息,這會兒就是正在等待接收信息的狀態,也就是等待喚醒,內核此刻將其掛起(不是傳統意義的掛起,還在內存裏,其實就是睡眠狀態,等個鬧鐘,或者有人叫醒)


等待到喚醒的過程:(類似於NSNotificationCenter,在收到Post時喚醒進行處理)

  • 指定用於喚醒的mach_port端口

  • 調用mach_msg監聽喚醒端口,被喚醒前,系統內核將此線程掛起,停留在mach_msg_trap狀態

  • 由另一個線程(或另一個進程中的某個線程)向內核發送這個端口的msg後,trap狀態被喚醒,RunLoop繼續運行

RunLoop迭代執行順序(僞代碼)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//設定過期時間  
SetupThisRunLoopRunTimeOutTimer();  //by GCD timer  
do{  
    //通知Observer要跑timer跟source  
    __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);  
    __CFRunLoopDoObservers(kCFRunLoopBeforeSources);  
       
    __CFRunLoopDoBlocks();  
    //運行到此刻,去檢測當前加到消息隊列source0的消息,此方法遍歷source0去執行  
    __CFRunLoopDoSource0();  
       
    //詢問GCD有沒有分到主線程的東西需要調用  
    CheckIfExistMessageInMainDispatchQueue();   //GCD  
       
    //通知Observer要進入睡眠  
    __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);  
    //此刻獲取到是哪個端口把我叫醒  
    var wakeUpPort = SleepAndWaitForWakingUpPorts();  
    //  mach_msg_trap  
    //  Zzz...  
    //  Received mach_msg,  wake up!  
       
    //通知Observer我要醒了~  
    __CFRunLoopDoObservers(kCFRunLoopAfterWaiting);  
    //Handler msgs  
    if(wakeUpPort == timerPort){  
        //如果是timer喚醒就去執行timer  
        __CFRunLoopDoTimer();  
    }else if(wakeUpPort == mainDispatchQueuePort){  
        //GCD需要我,就去調GCD的事件  
        __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE();  
    }else{  
        //比如說網絡來數據了就會用這個端口喚醒,然後做數據處理  
        __CFRunloopDoSource1();  
    }  
    __CFRunLoopDoBlocks();  
}while (!stop && !timeOut);//如果沒被外部幹掉或者時間沒到,繼續循環

其中var wakeUpPort = SleepAndWaitForWakingUpPorts();這句僞代碼可以看作是RunLoop的核心。內部實現簡化爲這樣:先調用__CFRunLoopServiceMachPort() ——> 裏面會調用mach_msg()函數 然後會卡在這裏,等待接收消息來喚醒RunLoop。直到下面的某個條件被觸發才被喚醒:

  • time_out 超時時間到了

  • 有一個Source事件

  • timer的時間到了

RunLoop 調用mach_msg()函數去接收消息,如果沒有其他 mach_port 發送消息過來,內核就會將線程置於等待狀態,直到接收到msg。就好比我們在一個函數中,調用了scanf()函數來接收輸入一樣,只有收到了輸入信息,代碼才能繼續向下執行,否則會一直卡在那裏。

AFNetworking中RunLoop的創建

這段代碼在AFURLConnectionOperation.m的157到174行

添加一個port監聽以達到常駐服務。比如,當我們的程序要提供語音服務的時候,就可以創建一個專門爲語音功能服務的線程,當需要語音服務的時候,這個線程就可以來執行。下圖是AFNetWorking的進程堆棧

一個TableView延遲加載圖片的新思維

這個問題是有的TableView有大量圖片(比如頭像)加載,在滑動的時候,請求網絡,下載完圖片之後設置的時候會卡,往常的解決方案一般是添加delegate之類的,檢測什麼時候滑動結束什麼時候去設置圖片

在知道RunLoop之後,可以採用下面的方案,在DefaultMode去做,這樣滑動的時候就不會調用設置圖片方法

1
2
3
4
5
UIImage *downLoadImage = ...;  
[self.avatarImageView performSelector:@selector(setImage:)  
                        withObject:downloadImage  
                        afterDelay:0  
                        inModes:@[NSDefaultRunLoopMode]];

讓Crash的App迴光返照

App崩潰的發生分兩種情況:


  1. program received signal:SIGABRT SIGABRT 一般是過度release 或者 發送 unrecogized selector導致。

  2. EXC_BAD_ACCESS 是訪問已被釋放的內存導致,野指針錯誤。

由 SIGABRT 引起的Crash 是系統發這個signal給App,程序收到這個signal後,就會把主線程的RunLoop殺死,程序就Crash了 該例只針對 SIGABRT引起的Crash有效

1
2
3
4
5
6
7
8
9
10
11
12
CFRunLoopRef runloop = CFRunLoopGetCurrent();  
    //獲取所有Mode,因爲可能有很多Mode,每個Mode都需要跑,此處可以選擇提交下崩潰信息之類的  
    UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"程序崩潰了" message:@"崩潰信息" delegate:nil cancelButtonTitle:@"取消" otherButtonTitles:nil];  
       
    [alertView show];  
    NSArray *allModes = CFBridgingRelease(CFRunLoopCopyAllModes(runloop));  
    while (1) {  
        //快速切換Mode  
        for (NSString *mode in allModes) {  
            CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);  
        }  
    }

接到Crash的Signal後手動重啓RunLoop

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章