概念
Runloop就像它的名字一樣,就是跑環.我的理解就是一個死循環.是一個可以隨時睡眠,隨時喚醒的死循環
大家可以想一下,手機app爲什麼會一直運行?而且在接收到用戶點擊等等操作時就會有所反映.這個離不開runloop.
iOS app啓動時就會啓動一個runloop,而且這種模式應該Android也有,所以纔會有了app能一直運行
每個線程都有一個runloop,但是隻有主線程的runloop是默認開啓的,其他子線程需要調用NSRunLoop *runloop = [NSRunLoop
currentRunLoop];
獲取runloop的同時就會創建runloop
一個線程可以創建多個runloop,但是隻能是嵌套模式.也就是一個線程只有一個根runloop
作用
-
使程序一直運行,並且接收用戶輸入等事件
-
決定程序什麼時候處理什麼事件
-
調用方面 解耦(比如用戶劃一下屏幕,會產生N個event事件,但是用戶不可能等着被調方全部執行完再進行下一步的動作,也就是會將此係列事件扔到一個消息隊列裏,每次再從消息隊列裏面取,主調方與被調方實現解耦)
-
節省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有以下幾種:
-
CFRunLoopDefaultMode: 這個是默認 Mode,也是空閒狀態。主線程通常在這個 Mode 下運行的。
-
UITrackingRunLoopMode: ScrollView滾動時候的模式。
-
UIInitializationRunLoopMode: 在剛啓動程序時進入的第一個 Mode,私有,啓動完成後就不再使用。
-
GSEventReceiveRunLoopMode: 接受系統事件的內部的Mode,這個Mode由GraphicsServices調用在CFRunLoopRunSpecific前面。通常用不到。
-
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崩潰的發生分兩種情況:
-
program received signal:SIGABRT SIGABRT 一般是過度release 或者 發送 unrecogized selector導致。
-
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