RunLoop的前世今生

leonlei發表於2017-12-19

一、 RunLoop初識

日常的開發工作中,我們幾乎很少注意RunLoop,因為我們基本上“用不到”RunLoop。包括我在內應該有很多人都不瞭解這個東西,只是聽說過。最近有空查了不少資料終於把RunLoop執行原理搞清楚了。
本文會對RunLoop的原理進行深入探討,但是不涉及底層的實現。
我們平時開發中的很多東西都和RunLoop相關,比如:

  • AutoreleasePool
  • NSTimer
  • 訊息通知
  • perform函式
  • 網路請求
  • dispatch呼叫
  • block回撥
  • KVO
  • 觸控事件以及各種硬體感測器

RunLoop機制貫穿整個App的生命週期的,這裡提前劇透個彩蛋:

我們都知道:如果主執行緒的RunLoop掛掉了,App也就掛掉了

BUT:   我們通過RunLoop機制可以讓崩潰的App繼續保持執行,非常英吹思婷!後面會有介紹。

轉載請註明出處:來自LeonLei的部落格http://www.gaoshilei.com

二、 RunLoop詳解

計算機處理任務有程式和執行緒的概念,安卓中一個應用可以開啟多個程式,而在iOS中一個App只能開啟一個程式,但是執行緒可以開啟多個。執行緒是用來處理事務的,多個執行緒處理事務是為了防止執行緒堵塞;一般來說一個執行緒一次只能執行一個任務,任務執行完成這個執行緒就會退出。
某些情況下我們需要這個執行緒一直執行著,不管有沒有任務執行(比方說App的主執行緒),所以需要一種機制來維持執行緒的生命週期,iOS中叫做RunLoop,安卓裡面的Looper機制和此類似。
為了讓執行緒不退出隨時候命處理事件而不退出,可以將邏輯簡化為下面的程式碼

do{
    var message = getNewmessages();//接收來自外部的訊息
    exec(message);//處理訊息任務
}while(0==isQuit)
複製程式碼

RunLoop實際上也是一個物件,這個物件管理了執行緒內部需要處理的事件和訊息,存在RunLoop的執行緒一直處於“訊息接收->等待->處理”的迴圈中,直到這個迴圈結束(RunLoop被釋放)。

1. 程式、執行緒、RunLoop之間的關係

這裡舉一個比較通俗易懂的例子:

  • 程式:工廠
  • 執行緒:流水線
  • RunLoop:生產線上面的主管

當工廠接到商家的訂單時,會將訂單生產的訊息(外界的event訊息)傳送給對應流水線上的主管(RunLoop),主管接收到訊息之後啟動這個流水線(喚醒執行緒)進行生產(執行緒處理事務)。如果這個流水線沒有主管,流水線將會被工廠銷燬。

需要注意的是,執行緒與RunLoop是一一對應的關係(對應關係儲存在一個全域性的Dictionary裡),執行緒建立之後是沒有RunLoop的(主執行緒除外),RunLoop的建立是發生在第一次獲取時。

蘋果不允許直接建立RunLoop,但是可以通過[NSRunLoop currentRunLoop]或者CFRunLoopGetCurrent()來獲取(如果沒有就會自動建立一個)。

一般開發中使用的RunLoop就是NSRunLoop和CFRunLoopRef,CFRunLoopRef屬於Core Foundation框架,提供的是C函式的API,是執行緒安全的,NSRunLoop是基於CFRunLoopRef的封裝,提供了物件導向的API,這些API不是執行緒安全的

由於NSRunLoop是基於CFRunLoop封裝的,下文關於RunLoop的原理討論都會基於CFRunLoop來進行。NSRunLoop和CFRunLoop所有類都是一一對應的關係。

2. RunLoop主要組成

CFRunLoop物件可以檢測某個task或者dispatch的輸入事件,當檢測到有輸入源事件,CFRunLoop將會將其加入到執行緒中進行處理。比方說使用者輸入事件、網路連線事件、週期性或者延時事件、非同步的回撥等。

RunLoop可以檢測的事件型別一共有3種,分別是CFRunLoopSource、CFRunLoopTimer、CFRunLoopObserver。可以通過CFRunLoopAddSource, CFRunLoopAddTimer或者CFRunLoopAddObserver新增相應的事件型別。

要讓一個RunLoop跑起來還需要run loop modes,每一個source, timer和observer新增到RunLoop中時必須要與一個模式(CFRunLoopMode)相關聯才可以執行。

上面是對於CFRunLoop官方文件的解釋,大致說明了RunLoop的工作原理。
RunLoop的主要組成部分如下:

  • Run Loop (CFRunLoopRef)
  • Run Loop Source(CFRunLoopSourceRef)
  • Run Loop Timer(CFRunLoopTimerRef)
  • Run Loop Observer(CFRunLoopObserverRef)
  • Run Loop Modes(CFRunLoopModeRef)

RunLoop共包含5個類,但公開的只有Source、Timer、Observer相關的三個類。 這5個類之間的關係關係:

RunLoop的前世今生

下面對這幾個部分作詳細的講解。

1. RunLoop Modes

Run Loop Mode就是流水線上能夠生產的產品型別,流水線在一個時刻只能在一種模式下執行,生產某一型別的產品。訊息事件就是訂單。

CFRunLoopMode 和 CFRunLoop的結構大致如下:

struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
    CFMutableSetRef _sources0;    // Set
    CFMutableSetRef _sources1;    // Set
    CFMutableArrayRef _observers; // Array
    CFMutableArrayRef _timers;    // Array
    ...
};

struct __CFRunLoop {
    CFMutableSetRef _commonModes;     // Set
    CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
    CFRunLoopModeRef _currentMode;    // Current Runloop Mode
    CFMutableSetRef _modes;           // Set
    ...
};
複製程式碼

一個RunLoop包含了多個Mode,每個Mode又包含了若干個Source/Timer/Observer。每次呼叫 RunLoop的主函式時,只能指定其中一個Mode,這個Mode被稱作CurrentMode。如果需要切換 Mode,只能退出Loop,再重新指定一個Mode進入。這樣做主要是為了分隔開不同Mode中的Source/Timer/Observer,讓其互不影響。下面是5種Mode

  • kCFDefaultRunLoopMode App的預設Mode,通常主執行緒是在這個Mode下執行
  • UITrackingRunLoopMode 介面跟蹤Mode,用於ScrollView追蹤觸控滑動,保證介面滑動時不受其他Mode影響
  • UIInitializationRunLoopMode 在剛啟動App時第進入的第一個Mode,啟動完成後就不再使用
  • GSEventReceiveRunLoopMode 接受系統事件的內部Mode,通常用不到
  • kCFRunLoopCommonModes 這是一個佔位用的Mode,不是一種真正的Mode

其中kCFDefaultRunLoopMode、UITrackingRunLoopMode是蘋果公開的,其餘的mode都是無法新增的。既然沒有CommonModes這個模式,那我們平時用的這行程式碼怎麼解釋呢?

[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; 
複製程式碼

什麼是CommonModes?

一個 Mode 可以將自己標記為"Common"屬性(通過將其 ModeName 新增到 RunLoop 的 "commonModes" 中)。每當 RunLoop 的內容發生變化時,RunLoop 都會自動將 _commonModeItems 裡的 Source/Observer/Timer 同步到具有 "Common" 標記的所有Mode裡 主執行緒的 RunLoop 裡有 kCFRunLoopDefaultMode 和 UITrackingRunLoopMode,這兩個Mode都已經被標記為"Common"屬性。當你建立一個Timer並加到DefaultMode時,Timer會得到重複回撥,但此時滑動一個 scrollView 時,RunLoop 會將 mode 切換為TrackingRunLoopMode,這時Timer就不會被回撥,並且也不會影響到滑動操作。
如果想讓scrollView滑動時Timer可以正常呼叫,一種辦法就是手動將這個 Timer 分別加入這兩個 Mode。另一種方法就是將 Timer 加入到CommonMode 中。

怎麼將事件加入到CommonMode?
我們呼叫上面的程式碼將 Timer 加入到CommonMode 時,但實際並沒有 CommonMode,其實系統將這個 Timer 加入到頂層的 RunLoop 的 commonModeItems 中。commonModeItems 會被 RunLoop 自動更新到所有具有"Common"屬性的 Mode 裡去。
這一步其實是系統幫我們將Timer加到了kCFRunLoopDefaultMode和UITrackingRunLoopMode中。

2. RunLoop Source

CFRunLoopSourceRef是事件源(輸入源),比如外部的觸控,點選事件和系統內部程式間的通訊等。
按照官方文件,Source的分類:

  • Port-Based Sources
  • Custom Input Sources
  • Cocoa Perform Selector Sources

Source有兩個版本:Source0 和 Source1(這麼風騷的名字不知道是誰想出來的)。 Source0: 非基於Port的,只包含了一個回撥(函式指標),它並不能主動觸發事件。使用時,你需要先呼叫 CFRunLoopSourceSignal(source),將這個 Source 標記為待處理,然後手動呼叫 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件。
Source1: 基於Port的,包含了一個 mach_port 和一個回撥(函式指標),被用於通過核心和其他執行緒相互傳送訊息。這種 Source 能主動喚醒 RunLoop 的執行緒。後面講到的AFNetwoeking建立常駐執行緒就是線上程中新增一個NSport來實現的。

3. RunLoop Timer

CFRunLoopTimerRef是基於時間的觸發器,基本上說的就是NSTimer,它受RunLoop的Mode影響(GCD的定時器不受RunLoop的Mode影響),當其加入到 RunLoop 時,RunLoop會註冊對應的時間點,當時間點到時,RunLoop會被喚醒以執行那個回撥。如果執行緒阻塞或者不在這個Mode下,觸發點將不會執行,一直等到下一個週期時間點觸發。

4. RunLoop Observer

CFRunLoopObserverRef 是觀察者,每個 Observer 都包含了一個回撥(函式指標),當 RunLoop 的狀態發生變化時,觀察者就能通過回撥接受到這個變化。可以觀測的時間點有以下幾個

enum CFRunLoopActivity {
    kCFRunLoopEntry                     = (1 << 0),    // 即將進入Loop   
    kCFRunLoopBeforeTimers 		= (1 << 1),    // 即將處理 Timer    	
    kCFRunLoopBeforeSources		= (1 << 2),    // 即將處理 Source  
    kCFRunLoopBeforeWaiting		= (1 << 5),    // 即將進入休眠     
    kCFRunLoopAfterWaiting 		= (1 << 6),    // 剛從休眠中喚醒   
    kCFRunLoopExit                      = (1 << 7),    // 即將退出Loop  
    kCFRunLoopAllActivities		= 0x0FFFFFFFU  // 包含上面所有狀態  
};
typedef enum CFRunLoopActivity CFRunLoopActivity;
複製程式碼

3. RunLoop 執行機制

這是我從別人部落格上面摘錄的一張圖片,詳細的描述了RunLoop執行機制

RunLoop的前世今生

每次執行緒執行RunLoop都會自動處理之前未處理的訊息,並且將訊息傳送給觀察者,讓事件得到執行。RunLoop執行時首先根據modeName找到對應mode,如果mode裡沒有source/timer/observer,直接返回。
流程如下:

Step1 通知觀察者 RunLoop 啟動(之後呼叫內部函式,進入Loop,下面的流程都在Loop內部do-while函式中執行)
Step2 通知觀察者: RunLoop 即將觸發 Timer 回撥。(kCFRunLoopBeforeTimers)
Step3 通知觀察者: RunLoop 即將觸發 Source0 回撥。(kCFRunLoopBeforeSources)
Step4 RunLoop 觸發 Source0 回撥。 Step5 如果有 Source1 處於等待狀態,直接處理這個 Source1 然後跳轉到第9步處理訊息。
Step6 通知觀察者:RunLoop 的執行緒即將進入休眠(sleep)。(kCFRunLoopBeforeWaiting)
Step7 呼叫 mach_msg 等待接受 mach_port 的訊息。執行緒將進入休眠, 直到被下面某一個事件喚醒

  1. 存在Source0被標記為待處理,系統呼叫CFRunLoopWakeUp喚醒執行緒處理事件
  2. 定時器時間到了
  3. RunLoop自身的超時時間到了
  4. RunLoop外部呼叫者喚醒

Step8 通知觀察者執行緒已經被喚醒 (kCFRunLoopAfterWaiting)
Step9 處理事件

  1. 如果一個 Timer 到時間了,觸發這個Timer的回撥
  2. 如果有dispatch到main_queue的block,執行block
  3. 如果一個 Source1 發出事件了,處理這個事件

事件處理完成進行判斷:

  1. 進入loop時傳入引數指明處理完事件就返回(stopAfterHandle)
  2. 超出傳入引數標記的超時時間(timeout)
  3. 被外部呼叫者強制停止__CFRunLoopIsStopped(runloop)
  4. source/timer/observer 全都空了__CFRunLoopModeIsEmpty(runloop, currentMode)

上面4個條件都不滿足,即沒超時、mode裡沒空、loop也沒被停止,那繼續loop。此時跳轉到步驟2繼續迴圈。

Step10 系統通知觀察者: RunLoop 即將退出。 滿足步驟9事件處理完成判斷4條中的任何一條,跳出do-while函式的內部,通知觀察者Loop結束。

三、 RunLoop實際應用

1. AutoreleasePool

App啟動之後,系統啟動主執行緒並建立了RunLoop,在 main thread 中註冊了兩個 observer ,回撥都是_wrapRunLoopWithAutoreleasePoolHandler()

1. 第一個observer

監聽了一個事件:

1. 即將進入Loop(kCFRunLoopEntry)

其回撥會呼叫 _objc_autoreleasePoolPush() 建立一個棧自動釋放池,這個優先順序最高,保證建立釋放池在其他操作之前。

2. 第二個observer

監聽了兩個事件:

1. 準備進入休眠(kCFRunLoopBeforeWaiting)

此時呼叫 _objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 來釋放舊的池並建立新的池。

2. 即將退出Loop(kCFRunLoopExit)

此時呼叫 _objc_autoreleasePoolPop()釋放自動釋放池。這個 observer 的優先順序最低,確保池子釋放在所有回撥之後。

在主執行緒中執行程式碼一般都是寫在事件回撥或Timer回撥中的,這些回撥都被加入了main thread的自動釋放池中,所以在ARC模式下我們不用關心物件什麼時候釋放,也不用去建立和管理pool。(如果事件不在主執行緒中要注意建立自動釋放池,否則可能會出現記憶體洩漏)。

2. 事件響應

系統註冊了一個 Source1 用來接收系統事件,其回撥函式為 __IOHIDEventSystemClientQueueCallback()。當一個硬體事件(觸控/鎖屏/搖晃等)發生後,首先由 IOKit.framework 生成一個 IOHIDEvent 事件並由 SpringBoard 接收,

SpringBoard 只接收按鍵(鎖屏/靜音等)、觸控、加速,感測器等幾種事件

隨後用 mach port 轉發給需要的App程式。隨後系統註冊的那個 Source1 就會觸發回撥,並呼叫 _UIApplicationHandleEventQueue()進行應用內部的分發。 _UIApplicationHandleEventQueue() 會把 IOHIDEvent 事件處理幷包裝成 UIEvent 進行處理或分發,其中包括識別 UIGesture/處理螢幕旋轉/傳送給 UIWindow 等。通常事件比如 UIButton 點選、touchesBegin/Move/End/Cancel 事件都是在這個回撥中完成的。

3. 定時器

1. NSTimer 的工作原理

這裡說的定時器就是NSTimer,我們使用頻率最高的定時器,它的原型是CFRunLoopTimerRef。一個Timer註冊 RunLoop 之後,RunLoop 會為這個Timer的重複時間點註冊好事件。
需要注意:

  1. 如果某個重複的時間點由於執行緒阻塞或者其他原因錯過了,這個時間點會跳過去,直到下一個可以執行的時間點才會觸發事件。舉個栗子:假如公交車的發車間隔是10分鐘,10:10的公交車我們沒趕上,只能等10:20,如果由於我打電話沒注意錯過了10:20的車,只能等10:30的。

  2. 我們在哪個執行緒呼叫 NSTimer 就必須在哪個執行緒終止

NSTimer有一個 tolerance ,官方文件給它的解釋是 Timer 的計時並不是準確的,有一定的誤差,這個誤差就是 tolerance 預設為0,我們可以手動設定這個誤差。文件最後還強調了,為了防止時間點偏移,系統有權力給這個屬性設定一個值無論你設定的值是多少,即使RunLoop 模式正確,當前執行緒並不阻塞,系統依然可能會在 NSTimer 上加上很小的的容差。

2. NSTimer 優化使用

我們在平時開發中一個很常見的現象:

在介面上有一個UIscrollview控制元件(tableview,collectionview等),如果此時還有一個定時器在執行一個事件,你會發現當你滾動scrollview的時候,定時器會失效。

這是因為,為了更好的使用者體驗,在主執行緒中UITrackingRunLoopMode的優先順序最高。在使用者拖動控制元件時,主執行緒的Run Loop是執行在UITrackingRunLoopMode下,而建立的Timer是預設關聯為Default Mode,因此係統不會立即執行Default Mode下接收的事件。

解決方法1:
將當前 Timer 加入到 UITrackingRunLoopMode 或 kCFRunLoopCommonModes 中

NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(TimerFire:) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];  
// 或 [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
[timer fire];
複製程式碼

解決方法2: 因為GCD建立的定時器不受RunLoop的影響,可以使用GCD建立的定時器

	//dispatch_source_t必須是全域性或static變數,否則timer不會觸發
    static dispatch_source_t timer;
    //建立新的排程源(這裡傳入的是DISPATCH_SOURCE_TYPE_TIMER,建立的是Timer排程
    timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"%@",[NSThread currentThread]);
    });
    //啟動或繼續定時器
    dispatch_resume(timer);
複製程式碼

3. 基於mode的擴充應用

在 Timer 使用中我們可以通過將其加入到不同的mode來解決 Timer 的跳票問題。不過有些情況下,例如:

使用者滑動 scrollView 的過程中載入圖片,由於UI的操作都是在主執行緒進行的,會造成滑動不流暢的問題,這個時候我們就需要在滑動的時候不載入圖片,等滑動操作完成再進行載入圖片的操作。

一般我們可以設定代理,當使用者滑動結束的時候通知代理載入圖片,這樣比較麻煩太low,基於RunLoop的原理我們只要一行程式碼即可搞定

UIImage *downloadImage = ...
[self.imageView performSelector:@selector(setImage:) 
						withObject: downloadImage 
						afterDelay:3.0 
						inModes:@[NSDefaultRunLoopMode]];
複製程式碼

通過將圖片的設定 setImage: 新增到 DefaultMode 裡面,確保在 UITrackingRunLoopMode 下該操作不會被執行,保證了滑動的流暢性。

4. 網路請求

1. 網路請求介面

iOS中的網路請求介面自下而上有這麼幾層

RunLoop的前世今生

CFSocket 是最底層的介面,只負責 socket 通訊。
CFNetwork 是基於 CFSocket 等介面的上層封裝,ASIHttpRequest 工作在這層。
NSURLConnection 是基於 CFNetwork 更高層的封裝,提供了物件導向的介面,AFNetworking 工作在這一層。
NSURLSession 看似是和 NSURLConnection 並列的,實際上它也用到了 NSURLConnection 的部分功能(比如 com.apple.NSURLConnectionLoader 執行緒)

開始網路傳輸時,NSURLConnection 建立了兩個新執行緒:com.apple.NSURLConnectionLoadercom.apple.CFSocket.private
其中 CFSocket 執行緒是處理底層 socket 連線的,NSURLConnectionLoader 這個執行緒的RunLoop 建立了一個 Source1 事件源用來監聽底層 socket 事件。當 CFSocket 處理好 socket 事件之後會通過 mach port 通知 NSURLConnectionLoader,然後 NSURLConnectionLoader 所在的執行緒再將訊息通過 mach prot 轉發給上層的 Delegate 所在的執行緒,同時喚醒 Delegate 執行緒的 RunLoop 來讓其處理這些通知。

2. AFNetworking 的工作原理

在AFNetworking2.6.3版本之前是有 AFURLConnectionOperation 這個類的, AFNetworking 3.0 版本開始已經移除了這個類,AFN沒有自己建立執行緒,而是採用的下面的這種方式

[inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];  

SCNetworkReachabilityUnscheduleFromRunLoop(self.networkReachability, CFRunLoopGetMain(), kCFRunLoopCommonModes);
複製程式碼

由於本文討論的是RunLoop,所以這裡我們還是回到2.6.3版本AFN自己建立執行緒並新增RunLoop的這種方式討論,在 AFURLConnectionOperation 類中可以找到下面的程式碼

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });

    return _networkRequestThread;
}
複製程式碼

從上面的程式碼可以看出,AFN建立了一個新的執行緒命名為 AFNetworking ,然後在這個執行緒中建立了一個 RunLoop ,在上面2.3章節 RunLoop 執行機制中提到了,一個RunLoop中如果source/timer/observer 都為空則會退出,並不進入迴圈。所以,AFN在這裡為 RunLoop 新增了一個 NSMachPort ,這個port開啟相當於新增了一個Source1事件源,但是這個事件源並沒有真正的監聽什麼東西,只是為了不讓 RunLoop 退出。

//開始請求
- (void)start {
    [self.lock lock];
    if ([self isCancelled]) {
        [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    } else if ([self isReady]) {
        self.state = AFOperationExecutingState;
        [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    }
    [self.lock unlock];
}
//暫停請求
- (void)pause {
    if ([self isPaused] || [self isFinished] || [self isCancelled]) {
        return;
    }
    [self.lock lock];
    if ([self isExecuting]) {
        [self performSelector:@selector(operationDidPause) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];

        dispatch_async(dispatch_get_main_queue(), ^{
            NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
            [notificationCenter postNotificationName:AFNetworkingOperationDidFinishNotification object:self];
        });
    }
    self.state = AFOperationPausedState;
    [self.lock unlock];
}
//取消請求
- (void)cancel {
    [self.lock lock];
    if (![self isFinished] && ![self isCancelled]) {
        [super cancel];
        if ([self isExecuting]) {
            [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
        }
    }
    [self.lock unlock];
}
複製程式碼

可以看到,AFN每次進行的網路操作,開始、暫停、取消操作時都將相應的執行任務扔進了自己建立的執行緒的 RunLoop 中進行處理,從而避免造成主執行緒的阻塞。

5. 處理崩潰讓程式繼續執行

我們都知道,如果App執行遇到 Exception 就會直接崩潰並且退出,其實真正讓應用退出的並不是產生的異常,而是當產生異常時,系統會結束掉當前主執行緒的 RunLoop ,RunLoop 退出主執行緒就退出了,所以應用才會退出。明白這個道理,去完成這個“不可能的任務”就很簡單了。

接下來我們就去讓應用在崩潰時依然可以正常執行,這個是非常有意義的。

1. 提升使用者體驗

應用遇到BUG崩潰時一般會給使用者造成非常不好的使用者體驗,如果當應用崩潰時我們讓使用者選擇退出還是繼續執行,那麼使用者會感覺我們的App跟別人的不一樣,叼叼噠!

2. 收集崩潰日誌

蘋果提供了產生 Exception 的處理方法,我們可以在相應的方法中處理產生的異常,但是這個時間非常的短,之後應用就會退出,具體多長時間我們也不清楚,很被動。如果我們可以在應用崩潰時,有足夠的時間收集並且上傳到伺服器,那麼給我們的分析和解決BUG會帶來相當大的便利。

下面直接上程式碼,非常簡單:

    CFRunLoopRef runLoop = CFRunLoopGetCurrent();
    CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);
    while (!isQuit){
        for (NSString *mode in (__bridge NSArray *)allModes) {
            CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
        }
    }
    CFRelease(allModes);
複製程式碼

把上面的程式碼新增到 Exception 的handle方法中,此時建立了一個 RunLoop ,讓這個 RunLoop 在所有的 Mode 下面一直不停的跑,保證主執行緒不會退出,我們的應用也就存活下來了。

參考:
developer.apple.com/reference/c…
iphil.cc/?p=279
blog.ibireme.com/2015/11/12/…
www.itdadao.com/article/251…

相關文章