什么是Runloop
- 為了實現(xiàn)線程能在有事件喚起的時候?qū)崟r處理Event,并且在沒有事件的時候進入休眠并不退出,繼續(xù)等待下一次事件消息的喚醒的機制,所以出現(xiàn)了和很多系統(tǒng)中Eventloop機制類似的Runloop機制。
Runloop的實質(zhì)
- RunLoop 實際上就是一個對象,這個對象管理了其需要處理的事件和消息,并提供了一個入口函數(shù)來執(zhí)行上面 Event Loop 的邏輯。線程執(zhí)行了這個函數(shù)后,就會一直處于這個函數(shù)內(nèi)部 “接受消息->等待->處理” 的循環(huán)中,直到這個循環(huán)結束(比如傳入 quit 的消息),函數(shù)返回。
Runloops 的功能
- 使程序一直運行并接受用戶輸入
- 決定程序在何時應該處理哪些Event
- 調(diào)用解耦 (SmallTalk/Message Queue)
- 節(jié)省CPU時間
Runloops的Cocoa實現(xiàn)
- Foundation 中實現(xiàn)了 NSRunloop 框架
- NSRunloop 基于 CFRunloop封裝而來,CFRunloop基于c/c++封裝,代碼開源
- 支持CFRunloop運行的核心內(nèi)容有 GCD, XNU的mach內(nèi)核,block回調(diào)機制,pthread等
- 依賴Runloop運行的框架、類、方法:NSTimer, UIEvent, Autorelease Pool, performThread和 performDelay等,CADisplayLink,CATransition,CAAnimation,dispatch_get_main_queue(),NSURLConnection,AFNetworking,
Runloop 的 類結構分析
1.CFRunLoopSourceRef
- Source 是 RunLoop的數(shù)據(jù)抽象類 (protocol)
- Runloop定義了兩個Version的Source(事件產(chǎn)生的地方):
- Source0: 基于非Port的事件,只包含了一個回調(diào)(函數(shù)指針),它并不能主動觸發(fā)事件。一般是App自己負責管理及觸發(fā)如UIEvent/CFSocket。使用時,你需要先調(diào)用 CFRunLoopSourceSignal(source),將這個 Source 標記為待處理,然后手動調(diào)用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件。
- Source1: 包含了一個 mach_port 和一個回調(diào)(函數(shù)指針),被用于通過內(nèi)核和其他線程相互發(fā)送消息.由RunLoop和內(nèi)核管理,Mach port驅(qū)動,如CFMachPort、CFMessagePort
2.CFRunloopTimerRef
基于時間的觸發(fā)器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一個時間長度和一個回調(diào)(函數(shù)指針)。當其加入到 RunLoop 時,RunLoop會注冊對應的時間點,當時間點到時,RunLoop會被喚醒以執(zhí)行那個回調(diào)。
3.CFRunloopObserverRef
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即將進入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7), // 即將退出Loop
};
RUNLoopObserver 與 AutoreleasePool
AutoreleasePool
App啟動后,蘋果在主線程 RunLoop 里注冊了兩個 Observer,其回調(diào)都是 _wrapRunLoopWithAutoreleasePoolHandler()。第一個 Observer 監(jiān)視的事件是 Entry(即將進入Loop),其回調(diào)內(nèi)會調(diào)用 _objc_autoreleasePoolPush() 創(chuàng)建自動釋放池。其 order 是-2147483647,優(yōu)先級最高,保證創(chuàng)建釋放池發(fā)生在其他所有回調(diào)之前。
第二個 Observer 監(jiān)視了兩個事件: BeforeWaiting(準備進入休眠) 時調(diào)用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池并創(chuàng)建新池;Exit(即將退出Loop) 時調(diào)用 _objc_autoreleasePoolPop() 來釋放自動釋放池。這個 Observer 的 order 是 2147483647,優(yōu)先級最低,保證其釋放池子發(fā)生在其他所有回調(diào)之后。
RunloopMode
- Runloop 在同一段時間只能且必須在一種特定的mode下Run
- 更換Mode時,必須Stop當前Loop,重齊新Loop
1.NSDefaulyRunLoopMode
默認狀態(tài)、空閑狀態(tài)
2.UITrackingRunLoopMode
滑動態(tài)mode -追蹤 ScrollView 滑動時的狀態(tài)
3.UIInitializationRunLoopMode
私有,啟動APP時切換進入,完成啟動就被拋棄不再使用
4.GSEventReceiveRunLoopMode
接受系統(tǒng)事件的內(nèi)部Mode,通常用不到。
5.NSRunLoopCommonModes
這個mode比較特殊,包含了DefaultMode和UITrackingRunLoopMode 的狀態(tài),一般作占位用,結構也比較特殊
應用: UITrakingRunloopMode 與 Timer
主線程的 RunLoop 里有兩個預置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。這兩個 Mode 都已經(jīng)被標記為”Common”屬性。DefaultMode 是 App 平時所處的狀態(tài),TrackingRunLoopMode 是追蹤 ScrollView 滑動時的狀態(tài)。當你創(chuàng)建一個 Timer 并加到 DefaultMode 時,Timer 會得到重復回調(diào),但此時滑動一個TableView時,RunLoop 會將 mode 切換為 TrackingRunLoopMode,這時 Timer 就不會被回調(diào),并且也不會影響到滑動操作。
// 默認添加到DefaultMode中
NSTimer *timer= [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerAction:) userInfo:nil repeats:YES];
// 添加到CommonModes中去
NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:1] interval:1 target:testObject4 selector:@selector(timerAction:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
RunLoopMode的切換
- Run RunLoop對象需要每次調(diào)用int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle)需要指定一個ModeName進入
- 當有滑動事件響應后,主線程的RunLoop對象會喚醒RunLoop對象,調(diào)用主函數(shù)傳入TrackingMode的Name切換到滑動Mode中處理相應的Source
Runloop 運行流程
/// 用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的實現(xiàn)
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
/// 首先根據(jù)modeName找到對應mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
/// 如果mode里沒有source/timer/observer, 直接返回。
if (__CFRunLoopModeIsEmpty(currentMode)) return;
/// 1. 通知 Observers: RunLoop 即將進入 loop。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
/// 內(nèi)部函數(shù),進入loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
Boolean sourceHandledThisLoop = NO;
int retVal = 0;
do {
/// 2. 通知 Observers: RunLoop 即將觸發(fā) Timer 回調(diào)。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: RunLoop 即將觸發(fā) Source0 (非port) 回調(diào)。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
/// 執(zhí)行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 4. RunLoop 觸發(fā) Source0 (非port) 回調(diào)。
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
/// 執(zhí)行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 5. 如果有 Source1 (基于port) 處于 ready 狀態(tài),直接處理這個 Source1 然后跳轉(zhuǎn)去處理消息。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
if (hasMsg) goto handle_msg;
}
/// 通知 Observers: RunLoop 的線程即將進入休眠(sleep)。
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}
/// 7. 調(diào)用 mach_msg 等待接受 mach_port 的消息。線程將進入休眠, 直到被下面某一個事件喚醒。
/// ? 一個基于 port 的Source 的事件。
/// ? 一個 Timer 到時間了
/// ? RunLoop 自身的超時時間到了
/// ? 被其他什么調(diào)用者手動喚醒
__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 到時間了,觸發(fā)這個Timer的回調(diào)。
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}
/// 9.2 如果有dispatch到main_queue的block,執(zhí)行block。
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}
/// 9.3 如果一個 Source1 (基于port) 發(fā)出事件了,處理這個事件
else {
CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
if (sourceHandledThisLoop) {
mach_msg(reply, MACH_SEND_MSG, reply);
}
}
/// 執(zhí)行加入到Loop的block
__CFRunLoopDoBlocks(runloop, currentMode);
if (sourceHandledThisLoop && stopAfterHandle) {
/// 進入loop時參數(shù)說處理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout) {
/// 超出傳入?yún)?shù)標記的超時時間了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(runloop)) {
/// 被外部調(diào)用者強制停止了
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
/// source/timer/observer一個都沒有了
retVal = kCFRunLoopRunFinished;
}
/// 如果沒超時,mode里沒空,loop也沒被停止,那繼續(xù)loop。
} while (retVal == 0);
}
/// 10. 通知 Observers: RunLoop 即將退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
- 掛起
/// 7. 調(diào)用 mach_msg 等待接受 mach_port 的消息。線程將進入休眠, 直到被下面某一個事件喚醒。
/// ? 一個基于 port 的Source 的事件。
/// ? 一個 Timer 到時間了
/// ? RunLoop 自身的超時時間到了
/// ? 被其他什么調(diào)用者手動喚醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
}
上面的函數(shù)主要實現(xiàn)了兩個步驟
1.指定用于喚醒port的端口
2.調(diào)用mach_msg監(jiān)聽喚醒端口,被喚醒前,將這個線程掛起,停留在mach_msg_trap狀態(tài)
- 喚醒
mach_msg(msg,MACH_RCV_MSG,port)
向指定端口發(fā)送port消息,喚醒Runloop
應用
tableViewCell 優(yōu)化
// 當滑動狀態(tài)下就不進行設置圖片操作
UIImage *download = ...;
[self.avatarImageView perfomSelector:@selector(setImage:) withObject:download afterDelay:0 inModes:@[NSDefaultRunloopMode]];
signal救回
CFRunloopRef runloop = CFRunloopGetCurrent();
NSArray *allModes = CFBridgingRelease(XFRunLoopCopyAllModes(runloop));
while(1){
for (NString *mode in allModes) {
CFRunloopRunInMode((CFStringRef)mode, 0.001, false);
}
}
可以做最后的收集挽回工作(彈窗提示等)
此處后面在崩潰信息處補充
一次觸摸事件的回應
具體流程參照應用 -- 圖形響應鏈
1.蘋果注冊了一個 Source1 (基于 mach port 的) 用來接收系統(tǒng)事件,其回調(diào)函數(shù)為 __IOHIDEventSystemClientQueueCallback()
- 當我們觸發(fā)了事件(觸摸/鎖屏/搖晃等)后,由IOKit.framework生成一個 IOHIDEvent事件
(IOKit是蘋果的硬件驅(qū)動框架,它專門處理用戶交互設備,由IOHIDServices和IOHIDDisplays兩部分組成,其中IOHIDServices是專門處理用戶交互的,它會將事件封裝成IOHIDEvents對象)
- 然后這些事件又由SpringBoard接收,它只接收收按鍵(鎖屏/靜音等),觸摸,加速,接近傳感器等幾種 Event
- 接著用mach_port轉(zhuǎn)發(fā)給需要的App進程.隨后蘋果注冊的那個Source1 就會接收IOHIDEvent,之后再回調(diào)__IOHIDEventSystemClientQueueCallback(),觸發(fā)一個Source0資源,Source觸發(fā)了回調(diào) _UIApplicationHandleEventQueue()進行應用內(nèi)部的分發(fā)。_UIApplicationHandleEventQueue()把IOHIDEvent處理包裝成UIEvent進行處理分發(fā),我們平時的UIGesture/處理屏幕旋轉(zhuǎn)/發(fā)送給 UIWindow/UIButton 點擊、touchesBegin/Move/End/Cancel這些事件,都是在這個回調(diào)中完成
- 當上面的 _UIApplicationHandleEventQueue() 識別了一個手勢時,其首先會調(diào)用 Cancel 將當前的 touchesBegin/Move/End 系列回調(diào)打斷。隨后系統(tǒng)將對應的 UIGestureRecognizer 標記為待處理。
- 蘋果注冊了一個 Observer 監(jiān)測 BeforeWaiting (Loop即將進入休眠) 事件,這個Observer的回調(diào)函數(shù)是 _UIGestureRecognizerUpdateObserver(),其內(nèi)部會獲取所有剛被標記為待處理的 GestureRecognizer,并執(zhí)行GestureRecognizer的回調(diào)。
- 當有 UIGestureRecognizer 的變化(創(chuàng)建/銷毀/狀態(tài)改變)時,這個回調(diào)都會進行相應處理。
該文的參考資料
1.深入理解RunLoop https://blog.ibireme.com/2015/05/18/runloop/
2. Runloop 線下分享 http://v-wb.youku.com/v_show/id_XODgxODkzODI0.html#paction