參考(抄襲)資料
- 深入理解RunLoop,by @Ibireme
- 孫源的線下分享視頻低清在線,高清無碼視頻,Key Note 文件,by @Sunnyxx
- 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
CFRunLoopRef 是在 CoreFoundation 框架內的,它提供了純 C 函數的 API,所有這些 API 都是線程安全的;
NSRunLoop 是基於 CFRunLoopRef 的封裝,提供了面向對象的 API,但是這些 API 不是線程安全的。
GCD 跟 RunLoop 之間存在一些協作關係;mach kernel 讓線程陷入休眠;block 爲 run loop 提供業務代碼;線程則更是 run loop 不可缺少的環節。
Example1
此外還有一些平時使用得多的類跟庫,也是依賴於 run loop 的:
- NSTimer: 每個 timer 都必須得添加到 run loop 中才能跑起來;
- UIEvent: 時間的產生,分發,到代碼執行都是通過 run loop 在跑的;
- Autorelease: 本次 run loop 結束時會將本次 loop 內產生的所有 Autorelease 對象釋放,事件大約在本次 run loop 休眠之後,下次 run loop 休眠之前的某個時間點。下面會提到;
- Selector: 要想在線程中執行 selector,線程中必須有一個正在運行的 run loop;
- NSDelayedPerforming:
performSelector:AfterDelay:
之類的函數,實際上其內部會創建一個 timer 並添加到當前線程的 run loop 中。所以如果當前線程沒有 run loop,則這個方法會失效。 - NSThreadPerformAddition: 跟 4. 同,另外實際上其會創建一個 Timer 加到對應的線程去,同樣的,如果對應線程沒有 RunLoop 該方法也會失效。(by Ibireme)
- 界面更新相關: 改變了 UI 的 frame 或者是 UIView/CALayer 的層次等等更新了界面之後,會在 run loop 的 observer 中執行實際的繪製和調整,下面會提到;
- dispatch_get_main_queue: block 會在主線程的 run loop 中得到執行,下詳;
- NSURLConnection: delegate 和網絡回來的數據都是在 run loop 中跑的,下詳;
Example2
看看一個 sample 樣例的調用堆棧:
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 機制
CFRunLoopSource
source 是 run loop 的數據源抽象類(id ),run loop 中存在兩個 version 的 source:
- source0,處理 APP 內部事件,APP 自己負責管理(觸發),例如 touch 事件,UIEvent,CFSocket。source0 只包含了一個回調(函數指針),它並不能主動觸發事件。使用時,你需要先調用
CFRunLoopSourceSignal(source)
,將這個 Source 標記爲待處理,然後手動調用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件。 - source1,由 run loop 和內核管理,Mach port 驅動,例如 CFMachPort,CFMessagePort。關於 port:給某個進程發消息可以發到某個 port 上,如果進程監聽這個 port,就可以收到這個消息。注意,是進程。一個 app 就是一個進程。
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 目前自身的狀態:
- kCFRunLoopEntry 開始進入 run loop 了;
- kCFRunLoopBeforeTimers 要執行 timer 了;
- kCFRunLoopBeforeSources 要執行 source 了;
- kCFRunLoopBeforeWaiting 將要睡眠了;
- kCFRunLoopAfterWaiting run loop 被喚醒了;
- kCFRunLoopExit run loop 退出了;
框架中的很多機制都是由 observer 來觸發的,例如 CAAnimation(BeforeWaiting或者AfterWaiting時、彙集整個loop的Animation一起執行)。可以看下面關於界面更新的內容。
上面的 Source/Timer/Observer 被統稱爲 mode item,一個 item 可以被同時加入多個 mode。但一個 item 被重複加入同一個 mode 時是不會有效果的。如果一個 mode 中一個 item 都沒有,則 RunLoop 會直接退出,不進入循環。
另外再看看 Observer 跟 Autorelease Pool (自動釋放池)之間的關係:
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 對他們的存取方式如下:
run loop 在同一個時間段只能在一種特定的 mode 下 run,如果需要更換 mode 的話,需要先停止(應該是退出?)當前 loop,然後重新啓動新 loop。mode 是 iOS App 滑動順暢的關鍵。有以下幾種 mode:
- NSDefaultRunLoopMode:默認的狀態,也是空閒的狀態——對 APP 進行除滑動外其餘操作或者無操作時,main run loop 就會處於這個 mode;
- UITrackingRunLoopMode:滑動 ScrollView 時會切換到這個 mode;
- UIInitializationRunLoopMode:私有,在 APP 啓動時會處於這個 mode,啓動後 切到 Default進行待機;
- 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];
開始滑動時,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 的關係
GCD 中 dispatch 到 main queue 的 block 是被分發到 main run loop 中執行的。這是由於 GCD 中的主線程跟 run loop 中的主線程是同一個。
假如使用 GCD 中的 dispatch_after,當時間到了之後,dispatch_after 纔會將 block 放到 run loop 中去執行。
RunLoop 的掛起和喚醒
上面的 mach_msg
跟 mach_msg_trap
是指定某個 mach_port 然後發給內核的,trap 就是一個等待的消息,表示等待被喚醒,於是 run loop 便會暫停而被掛起。
掛起與喚醒過程:
- 在 run loop 進入等待前,先要指定一個用於喚醒的 mach_port
- 然後調用
mach_msg
監聽喚醒端口。被喚醒前,系統內核將這個線程掛起,停留在 mach_msg_trap 狀態 - 由另一個線程(或者另一個進程中的某個線程)向內核發送這個端口的 msg,trap 狀態被喚醒,run loop 繼續還是處理任務
RunLoop 迭代執行順序
- 第一行設置過期時間,這是通過 GCD 的 timer 來監測的;
- 通知 observer 相關 run loop 狀態;
- 執行 block,執行添加到 run loop 中的 source0;
- 向 GCD 查詢是否有需要分派到主線程的任務;
- 進入休眠,通知 observer 即將進入休眠了;
- SleepAndWaitForWakingUpPorts() 讓線程進入休眠,等待消息來喚醒,即上面提到的 mach_msg_trap 狀態;
- 當消息來了,於是 wakeUpPort 得到返回值,根據返回值來執行業務處理;
根據蘋果的官方文檔的描述,執行流程如下:
這裏是 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
注意這行代碼:
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
networkRequestThread
創建一個單例線程,線程跑起來之後先去跑 networkRequestThreadEntryPoint:
,然後在這個函數中創建這個線程的 run loop。新建完的 runLoop 如果沒有事件處理的話就會直接退出了,所以讓它隨便監聽一個 port,讓它等待,一直活着。所以這個線程就可以一直駐留。這是一個創建常駐服務線程的好方法。
從調用堆棧可以看到,線程執行入口函數創建了 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>