RunLoop的前世此生

1、 RunLoop初識

平常的開發工做中,咱們幾乎不多注意RunLoop,由於咱們基本上「用不到」RunLoop。包括我在內應該有不少人都不瞭解這個東西,只是據說過。最近有空查了很多資料終於把RunLoop運行原理搞清楚了。
本文會對RunLoop的原理進行深刻探討,可是不涉及底層的實現。
咱們平時開發中的不少東西都和RunLoop相關,好比:ios

  • AutoreleasePool
  • NSTimer
  • 消息通知
  • perform函數
  • 網絡請求
  • dispatch調用
  • block回調
  • KVO
  • 觸摸事件以及各類硬件傳感器

RunLoop機制貫穿整個App的生命週期的,這裏提早劇透個彩蛋:安全

咱們都知道:若是主線程的RunLoop掛掉了,App也就掛掉了bash

BUT:   咱們經過RunLoop機制可讓崩潰的App繼續保持運行,很是英吹思婷!後面會有介紹。服務器

轉載請註明出處:來自LeonLei的博客http://www.gaoshilei.com網絡

2、 RunLoop詳解

計算機處理任務有進程和線程的概念,安卓中一個應用能夠開啓多個進程,而在iOS中一個App只能開啓一個進程,可是線程能夠開啓多個。線程是用來處理事務的,多個線程處理事務是爲了防止線程堵塞;通常來講一個線程一次只能執行一個任務,任務執行完成這個線程就會退出。
某些狀況下咱們須要這個線程一直運行着,無論有沒有任務執行(比方說App的主線程),因此須要一種機制來維持線程的生命週期,iOS中叫作RunLoop,安卓裏面的Looper機制和此相似。
爲了讓線程不退出隨時候命處理事件而不退出,能夠將邏輯簡化爲下面的代碼app

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

RunLoop實際上也是一個對象,這個對象管理了線程內部須要處理的事件和消息,存在RunLoop的線程一直處於「消息接收->等待->處理」的循環中,直到這個循環結束(RunLoop被釋放)。框架

1. 進程、線程、RunLoop之間的關係

這裏舉一個比較通俗易懂的例子:異步

  • 進程:工廠
  • 線程:流水線
  • RunLoop:生產線上面的主管

當工廠接到商家的訂單時,會將訂單生產的消息(外界的event消息)發送給對應流水線上的主管(RunLoop),主管接收到消息以後啓動這個流水線(喚醒線程)進行生產(線程處理事務)。若是這個流水線沒有主管,流水線將會被工廠銷燬。socket

須要注意的是,線程與RunLoop是一一對應的關係(對應關係保存在一個全局的Dictionary裏),線程建立以後是沒有RunLoop的(主線程除外),RunLoop的建立是發生在第一次獲取時。async

蘋果不容許直接建立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個類之間的關係關係:

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

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運行時首先根據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結束。

3、 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中的網絡請求接口自下而上有這麼幾層

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…

相關文章
相關標籤/搜索