RunLoop解析(視頻+原版文字)

參考(抄襲)資料

  1. 深入理解RunLoop,by @Ibireme
  2. 孫源的線下分享視頻低清在線高清無碼視頻Key Note 文件,by @Sunnyxx  
  3. RunLoop 的蘋果官方文檔



建議搭配以上資料輔助閱讀


RunLoop 是什麼鬼

首先,在一般情況下,代碼的執行是線性的,執行完成之後就會退出返回:

int main(int argc, char *argv[]) {
	NSLog(@"hello world");
	return 0;
}


通常我們創建線程來處理自己的任務,也是這樣的線性執行流程,當我們任務完成之後,便退出然後銷燬線程。


但是對於一個 APP 來說,這種線性的執行流程,就不適用了。總不能讓 APP 一打開,然後顯示一下第一個頁面,接着就馬上退出了吧。得想一種辦法,讓 APP 的主線程能夠一直駐留。在用戶觸發事件的時候,對其做出響應;在 APP 空閒的時候進入休眠,停止佔用 CPU。這種模型通常被稱爲 Event Loop,事件循環。Run Loop 實現了這種事件處理機制。事件驅動型代碼結構一般形式如下:

int main(int argc, char * argv[]) {
	while(AppIsRunning) {
		id whoWakesMe = SleepForWakingUp();
		id event = GetEvent(whoWakesMe);
		HandleEvent(event);
	}

	return 0;
}

Event Loop 結構中的重點有兩部分:

  • 外部的 while 循環結構,它保證了線程在處理完事件之後不會退出;
  • SleepForWakingUp 函數,讓線程在沒有事件需要處理的時候陷入休眠,讓出 CPU。當沒有事件需要處理時,代碼的實行會停在這個函數的調用處,線程在這裏進入休眠狀態;當事件到來時,線程被激活,whoWakesMe 獲得返回值,代碼從原來的休眠處重新跑起來,執行餘下的操作。

再舉一個簡單的例子,名爲“程序猿的 main thread”:

// by @Sunnyxx
while(活着) {
	有事幹了 = 我睡覺了沒事別叫我();

	if (該搬磚了) {
		搬磚();
	} else if (該吃飯了) {
		吃飯();
	} else if (該陪妹子了) {
		@throw(沒有妹子);
	}
}


在這裏,我不負責任地用自己的話總結一下:run loop 是一種消息處理機制,它讓線程能一直駐留而不退出,並且在閒時休眠,在事件到達時處理事件

RunLoop 實際上就是一個對象,這個對象管理了其需要處理的事件和消息,並提供了一個入口函數來執行上面 Event Loop 的邏輯。線程執行了這個函數後,就會一直處於這個函數內部 “接受消息->等待->處理” 的循環中,直到這個循環結束(比如傳入 quit 的消息),然後這個入口函數返回。(by ibireme)


因爲有了 run loop 的存在,使得:

  • 程序能一直運行並接受用戶輸入
  • 決定程序在何時該處理哪些事件
  • 調用解耦:例如主調方產生事件之後放入消息隊列,讓被調方自己取來處理,而不必等待被調方返回
  • 節省 CPU 時間:沒事件處理時休眠


CFRunLoopRef 的源代碼是開源,可以在這個鏈接下載到整個 CoreFoundation 的源碼。爲了方便跟蹤和查看,你可以新建一個 Xcode 工程,把這堆源碼拖進去看。

Run Loops in Cocoa

image


CFRunLoopRef 是在 CoreFoundation 框架內的,它提供了純 C 函數的 API,所有這些 API 都是線程安全的;
NSRunLoop 是基於 CFRunLoopRef 的封裝,提供了面向對象的 API,但是這些 API 不是線程安全的。


GCD 跟 RunLoop 之間存在一些協作關係;mach kernel 讓線程陷入休眠;block 爲 run loop 提供業務代碼;線程則更是 run loop 不可缺少的環節。

Example1

此外還有一些平時使用得多的類跟庫,也是依賴於 run loop 的:
image

  1. NSTimer: 每個 timer 都必須得添加到 run loop 中才能跑起來;
  2. UIEvent: 時間的產生,分發,到代碼執行都是通過 run loop 在跑的;
  3. Autorelease: 本次 run loop 結束時會將本次 loop 內產生的所有 Autorelease 對象釋放,事件大約在本次 run loop 休眠之後,下次 run loop 休眠之前的某個時間點。下面會提到;
  4. Selector: 要想在線程中執行 selector,線程中必須有一個正在運行的 run loop;
  5. NSDelayedPerforming: performSelector:AfterDelay: 之類的函數,實際上其內部會創建一個 timer 並添加到當前線程的 run loop 中。所以如果當前線程沒有 run loop,則這個方法會失效。
  6. NSThreadPerformAddition: 跟 4. 同,另外實際上其會創建一個 Timer 加到對應的線程去,同樣的,如果對應線程沒有 RunLoop 該方法也會失效。(by Ibireme)
  7. 界面更新相關: 改變了 UI 的 frame 或者是 UIView/CALayer 的層次等等更新了界面之後,會在 run loop 的 observer 中執行實際的繪製和調整,下面會提到;
  8. dispatch_get_main_queue: block 會在主線程的 run loop 中得到執行,下詳;
  9. NSURLConnection: delegate 和網絡回來的數據都是在 run loop 中跑的,下詳;
Example2

看看一個 sample 樣例的調用堆棧:
image
start 是 dyld 乾的,將程序調起來,然後是 main 函數,調用 UIApplicationMain 並返回。接着 Graphics Services 是處理硬件輸入的,比如點擊,所有的 UI 事件都是由它發出來的。接下來是 run loop ,最後是 UI 事件。


主線程中幾乎所有函數都是從以下六個之一的函數調起的:


__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__


長得比較醜陋,當然,這麼長串的名字是爲了在調用棧裏面自解釋。上面的函數都是 “Call Out”,通俗來講就是調出,往上層調用。

RunLoop 機制

image


CFRunLoopSource

source 是 run loop 的數據源抽象類(id ),run loop 中存在兩個 version 的 source:

  1. source0,處理 APP 內部事件,APP 自己負責管理(觸發),例如 touch 事件,UIEvent,CFSocket。source0 只包含了一個回調(函數指針),它並不能主動觸發事件。使用時,你需要先調用 CFRunLoopSourceSignal(source),將這個 Source 標記爲待處理,然後手動調用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件。
  2. source1,由 run loop 和內核管理,Mach port 驅動,例如 CFMachPort,CFMessagePort。關於 port:給某個進程發消息可以發到某個 port 上,如果進程監聽這個 port,就可以收到這個消息。注意,是進程。一個 app 就是一個進程。

image

source version 0 的內部結構


source 結構內部有一個聯合體,version0 中的結構中,成員主要都是各種函數指針,這些都是 run loop 需要調用的方法。如果自己實現一個 source 的話需要一個一個填進去。重要的方法是最後一個 perform 方法,裏面具體進行業務處理(此處從剛纔Button的堆棧中也能體現出來)。

CFRunLoopObserver

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),
    kCFRunLoopBeforeTimers = (1UL << 1),
    kCFRunLoopBeforeSources = (1UL << 2),
    kCFRunLoopBeforeWaiting = (1UL << 5),
    kCFRunLoopAfterWaiting = (1UL << 6),
    kCFRunLoopExit = (1UL << 7),
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};


kCFRunLoopEntry 開始進入 run loop 了;使用 Observer 肯定會需要用到上面這個枚舉,run loop 利用他們來告知 observer 目前自身的狀態:使用 Observer 肯定會需要用到上面這個枚舉,run loop 利用他們來告知 observer 目前自身的狀態:

  1. kCFRunLoopEntry 開始進入 run loop 了;
  2. kCFRunLoopBeforeTimers 要執行 timer 了;
  3. kCFRunLoopBeforeSources 要執行 source 了;
  4. kCFRunLoopBeforeWaiting 將要睡眠了;
  5. kCFRunLoopAfterWaiting run loop 被喚醒了;
  6. kCFRunLoopExit run loop 退出了;

框架中的很多機制都是由 observer 來觸發的,例如 CAAnimation(BeforeWaiting或者AfterWaiting時、彙集整個loop的Animation一起執行)。可以看下面關於界面更新的內容。


上面的 Source/Timer/Observer 被統稱爲 mode item,一個 item 可以被同時加入多個 mode。但一個 item 被重複加入同一個 mode 時是不會有效果的。如果一個 mode 中一個 item 都沒有,則 RunLoop 會直接退出,不進入循環。


另外再看看 Observer 跟 Autorelease Pool (自動釋放池)之間的關係:
image


RunLoop 與 線程之間的關係:

CFRunLoop 與 Thread 之間是一一對應的,但不是說一個線程只能起一個 run loop,可以多個嵌套。RunLoop 不能直接創建,它只提供了兩個自動獲取的函數:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。 這兩個函數內部的邏輯大概是下面這樣:


/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 訪問 loopsDic 時的鎖
static CFSpinLock_t loopsLock;
 
/// 獲取一個 pthread 對應的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
    OSSpinLockLock(&loopsLock);
    
    if (!loopsDic) {
        // 第一次進入時,初始化全局Dic,並先爲主線程創建一個 RunLoop。
        loopsDic = CFDictionaryCreateMutable();
        CFRunLoopRef mainLoop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
    }
    
    /// 直接從 Dictionary 裏獲取。
    CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
    
    if (!loop) {
        /// 取不到時,創建一個
        loop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, thread, loop);
        /// 註冊一個回調,當線程銷燬時,順便也銷燬其對應的 RunLoop。
        _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
    }
    
    OSSpinLockUnLock(&loopsLock);
    return loop;
}
 
CFRunLoopRef CFRunLoopGetMain() {
    return _CFRunLoopGet(pthread_main_thread_np());
}
 
CFRunLoopRef CFRunLoopGetCurrent() {
    return _CFRunLoopGet(pthread_self());
}


線程和 RunLoop 之間是一一對應的,其關係是保存在一個全局的 Dictionary 裏。線程剛創建時並沒有 RunLoop,如果你不主動獲取,那它一直都不會有。RunLoop 的創建是發生在第一次獲取時,RunLoop 的銷燬是發生在線程結束時。你只能在一個線程的內部獲取其 RunLoop(主線程除外)、子線程中默認是沒有RunLoop的。

RunLoop Mode

run loop 必須在某種模式下來跑,系統預定義了幾種模式。它們並不是一個 filter 的作用。mode 其實是一個 “樹枝節點” ,Source、Observer以及Timer的幾個節點實際上是在 mode 裏面的。mode 對他們的存取方式如下:
image


run loop 在同一個時間段只能在一種特定的 mode 下 run,如果需要更換 mode 的話,需要先停止(應該是退出?)當前 loop,然後重新啓動新 loop。mode 是 iOS App 滑動順暢的關鍵。有以下幾種 mode:

  1. NSDefaultRunLoopMode:默認的狀態,也是空閒的狀態——對 APP 進行除滑動外其餘操作或者無操作時,main run loop 就會處於這個 mode;
  2. UITrackingRunLoopMode:滑動 ScrollView 時會切換到這個 mode;
  3. UIInitializationRunLoopMode:私有,在 APP 啓動時會處於這個 mode,啓動後 切到 Default進行待機;
  4. NSRunLoopCommonModes:默認情況下包含 1 與 2 兩種 mode。也可以自己定義Mode添加到其中(基本不會出現);


經典問題,UITrackingRunLoopMode 與 Timer:


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


在主線程中調用上面的方法時, timer 默認被添加到 NSDefaultRunLoopMode 中,如果 scrollView 發生滑動,main run loop 會切換到 UITrackingRunLoopMode 下,於是 timer 便不會工作。如果要解決這個問題,可以將 timer 添加到 NSRunLoopCommonModes 中:

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



另在再來看看 RunLoopMode 切換時的調用堆棧:
image


開始滑動時,run loop 停止,然後利用 pushRunLoopMode 將 run loop 切換到 tracking mode 下;滑動停止,利用 popRunLoopMode 將 run loop 恢復回原來的模式(RunLoop始終是一個)。


CFRunLoop對外暴露的管理 Mode 接口只有下面2個:

CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);


當你調用 CFRunLoopRunInMode() 時,線程就會一直停留在這個循環裏;直到超時或被手動停止,該函數纔會返回。


Mode 暴露的管理 mode item 的接口有下面幾個:


CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);


你只能通過 mode name 來操作內部的 mode,當你傳入一個新的 mode name 但 RunLoop 內部沒有對應 mode 時,RunLoop會自動幫你創建對應的 CFRunLoopModeRef。對於一個 RunLoop 來說,其內部的 mode 只能增加不能刪除。

RunLoop Timer

NSTimer 是對 CFRunLoopTimer 的上層封裝(在上層調用的是內核MKTimer)。包括 performSelector:afterDelay: 裏面使用的也是 RunLoopTimer。CFRunLoopTimerRef 是基於時間的觸發器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一個時間長度和一個回調(函數指針)。當其加入到 RunLoop 時,RunLoop會註冊對應的時間點,當時間點到時,RunLoop會被喚醒以執行那個回調。

此外、RunLoop的Timer與GCD的Timer均爲獨立運行。

RunLoop 與 GCD 的關係

image


GCD 中 dispatch 到 main queue 的 block 是被分發到 main run loop 中執行的。這是由於 GCD 中的主線程跟 run loop 中的主線程是同一個。


假如使用 GCD 中的 dispatch_after,當時間到了之後,dispatch_after 纔會將 block 放到 run loop 中去執行。

RunLoop 的掛起和喚醒

image


上面的 mach_msg 跟 mach_msg_trap 是指定某個 mach_port 然後發給內核的,trap 就是一個等待的消息,表示等待被喚醒,於是 run loop 便會暫停而被掛起。


掛起與喚醒過程:

  • 在 run loop 進入等待前,先要指定一個用於喚醒的 mach_port
  • 然後調用 mach_msg 監聽喚醒端口。被喚醒前,系統內核將這個線程掛起,停留在 mach_msg_trap 狀態
  • 由另一個線程(或者另一個進程中的某個線程)向內核發送這個端口的 msg,trap 狀態被喚醒,run loop 繼續還是處理任務
RunLoop 迭代執行順序

image

RunLoop 迭代執行順序僞代碼

  • 第一行設置過期時間,這是通過 GCD 的 timer 來監測的;
  • 通知 observer 相關 run loop 狀態;
  • 執行 block,執行添加到 run loop 中的 source0;
  • 向 GCD 查詢是否有需要分派到主線程的任務;
  • 進入休眠,通知 observer 即將進入休眠了;
  • SleepAndWaitForWakingUpPorts() 讓線程進入休眠,等待消息來喚醒,即上面提到的 mach_msg_trap 狀態;
  • 當消息來了,於是 wakeUpPort 得到返回值,根據返回值來執行業務處理;


根據蘋果的官方文檔的描述,執行流程如下:
image
這裏是 ibireme 的另一份僞代碼:


/// 用DefaultMode啓動
void CFRunLoopRun(void) {
    CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}
 
/// 用指定的Mode啓動,允許設置RunLoop超時時間
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
 
/// RunLoop的實現
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
    
    /// 首先根據modeName找到對應mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
    /// 如果mode裏沒有source/timer/observer, 直接返回。
    if (__CFRunLoopModeIsEmpty(currentMode)) return;
    
    /// 1. 通知 Observers: RunLoop 即將進入 loop。
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
    
    /// 內部函數,進入loop
    __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
        
        Boolean sourceHandledThisLoop = NO;
        int retVal = 0;
        do {
 
            /// 2. 通知 Observers: RunLoop 即將觸發 Timer 回調。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
            /// 3. 通知 Observers: RunLoop 即將觸發 Source0 (非port) 回調。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
            /// 執行被加入的block
            __CFRunLoopDoBlocks(runloop, currentMode);
            
            /// 4. RunLoop 觸發 Source0 (非port) 回調。
            sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
            /// 執行被加入的block
            __CFRunLoopDoBlocks(runloop, currentMode);
 
            /// 5. 如果有 Source1 (基於port) 處於 ready 狀態,直接處理這個 Source1 然後跳轉去處理消息。
            if (__Source0DidDispatchPortLastTime) {
                Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
                if (hasMsg) goto handle_msg;
            }
            
            /// 通知 Observers: RunLoop 的線程即將進入休眠(sleep)。
            if (!sourceHandledThisLoop) {
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
            }
            
            /// 7. 調用 mach_msg 等待接受 mach_port 的消息。線程將進入休眠, 直到被下面某一個事件喚醒。
            /// · 一個基於 port 的Source 的事件。
            /// · 一個 Timer 到時間了
            /// · RunLoop 自身的超時時間到了
            /// · 被其他什麼調用者手動喚醒
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
                mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
            }
 
            /// 8. 通知 Observers: RunLoop 的線程剛剛被喚醒了。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
            
            /// 收到消息,處理消息。
            handle_msg:
 
            /// 9.1 如果一個 Timer 到時間了,觸發這個Timer的回調。
            if (msg_is_timer) {
                __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
            } 
 
            /// 9.2 如果有dispatch到main_queue的block,執行block。
            else if (msg_is_dispatch) {
                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            } 
 
            /// 9.3 如果一個 Source1 (基於port) 發出事件了,處理這個事件
            else {
                CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
                sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
                if (sourceHandledThisLoop) {
                    mach_msg(reply, MACH_SEND_MSG, reply);
                }
            }
            
            /// 執行加入到Loop的block
            __CFRunLoopDoBlocks(runloop, currentMode);
            
 
            if (sourceHandledThisLoop && stopAfterHandle) {
                /// 進入loop時參數說處理完事件就返回。
                retVal = kCFRunLoopRunHandledSource;
            } else if (timeout) {
                /// 超出傳入參數標記的超時時間了
                retVal = kCFRunLoopRunTimedOut;
            } else if (__CFRunLoopIsStopped(runloop)) {
                /// 被外部調用者強制停止了
                retVal = kCFRunLoopRunStopped;
            } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
                /// source/timer/observer一個都沒有了
                retVal = kCFRunLoopRunFinished;
            }
            
            /// 如果沒超時,mode裏沒空,loop也沒被停止,那繼續loop。
        } while (retVal == 0);
    }
    
    /// 10. 通知 Observers: RunLoop 即將退出。
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}


RunLoop 實踐
AFNetWorking

image


注意這行代碼:


[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];


networkRequestThread 創建一個單例線程,線程跑起來之後先去跑 networkRequestThreadEntryPoint:,然後在這個函數中創建這個線程的 run loop。新建完的 runLoop 如果沒有事件處理的話就會直接退出了,所以讓它隨便監聽一個 port,讓它等待,一直活着。所以這個線程就可以一直駐留。這是一個創建常駐服務線程的好方法。


image


從調用堆棧可以看到,線程執行入口函數創建了 run loop 之後,停在 mach_msg_trap 狀態,線程進入休眠。

TableView 延時加載圖片

網絡圖片下載完成之後去設置 cell 中的 imageView,會導致主線程“卡一下”。解決這個問題的最簡單的方法,就是將設置圖片的代碼放到 NSDefaultRunLoopMode 中去運行:


UIImage *downloadedImage = ...;
[self.avatarImageView performSelector:@selector(setImage:)
                           withObject:downloadedImage
                           afterDelay:0
                              inModes:@[NSDefaultRunLoopMode]];


於是在滑動時不會設置 imageView,直到滑動停止 mode 切換爲 defaultMode 纔會執行設置 image 的代碼。

讓 Crash 掉的 APP 迴光返照


//取當前 run loop
CFRunLoopRef runLoop = CFRunLoopGetCurrent();

//取 run loop 所有運行的 mode
NSArray *allModes = CFBridgingRelease(CFRunLoopCopyAllModes(runLoop));
while (1) {
    for (NSString *mode in allModes) {
    	//在每個 mode 中輪流運行至少 0.001 秒
        CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
    }
}


對於因爲接收到 crash 的 signal 而掛掉的程序,可以在接收到 crash 的信號之後重新起一個 run loop 然後跑起來。但是這個並不能保證 app 能像原來一樣能正常運行,只能是利用它來在奄奄一息的狀態下彈出一些友好的錯誤信息。

Async Test Case

原來寫 test case 時最大的問題就是,它不支持異步。當時的一種解決方法是”每0.0001秒驗證”:


- (void)runUntilBlock:(BOOL(^)())block timeout:(NSTimeInterval)timeout
{
    NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:timeout];
    do {
        CFTimeInterval quantum = 0.0001;
        CFRunLoopRunInMode(kCFRunLoopDefaultMode, quantum, false);
    } while([timeoutDate timeIntervalSinceNow] > 0.0 && !block());
}


這是原來的方案,後來更新了,換成了 run loop sleep 前驗證:

- (BOOL)runUntilBlock:(BOOL(^)())block timeout:(NSTimeInterval)timeout
{
    __block Boolean fulfilled = NO;
    void (^beforeWaiting) (CFRunLoopObserverRef observer, CFRunLoopActivity activity) =
    ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        fulfilled = block();
        if (fulfilled) {
            CFRunLoopStop(CFRunLoopGetCurrent());
        }
    };
    
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopBeforeWaiting, true, 0, beforeWaiting);
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    
    // Run!
    CFRunLoopRunInMode(kCFRunLoopDefaultMode, timeout, false);
    
    CFRunLoopRemoveObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    CFRelease(observer);
    
    return fulfilled;<span style="font-family: Arial, Helvetica, sans-serif;">}</span>

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