Runloop 理解總結

什么是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)生的地方):
    1. Source0: 基于非Port的事件,只包含了一個回調(diào)(函數(shù)指針),它并不能主動觸發(fā)事件。一般是App自己負責管理及觸發(fā)如UIEvent/CFSocket。使用時,你需要先調(diào)用 CFRunLoopSourceSignal(source),將這個 Source 標記為待處理,然后手動調(diào)用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件。
    2. 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

  1. Runloop 在同一段時間只能且必須在一種特定的mode下Run
  2. 更換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的切換
  1. Run RunLoop對象需要每次調(diào)用int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle)需要指定一個ModeName進入
  2. 當有滑動事件響應后,主線程的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);
  1. 掛起
/// 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)
  1. 喚醒
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()

  1. 當我們觸發(fā)了事件(觸摸/鎖屏/搖晃等)后,由IOKit.framework生成一個 IOHIDEvent事件

    (IOKit是蘋果的硬件驅(qū)動框架,它專門處理用戶交互設備,由IOHIDServices和IOHIDDisplays兩部分組成,其中IOHIDServices是專門處理用戶交互的,它會將事件封裝成IOHIDEvents對象)

  2. 然后這些事件又由SpringBoard接收,它只接收收按鍵(鎖屏/靜音等),觸摸,加速,接近傳感器等幾種 Event
  3. 接著用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)中完成
  4. 當上面的 _UIApplicationHandleEventQueue() 識別了一個手勢時,其首先會調(diào)用 Cancel 將當前的 touchesBegin/Move/End 系列回調(diào)打斷。隨后系統(tǒng)將對應的 UIGestureRecognizer 標記為待處理。
  5. 蘋果注冊了一個 Observer 監(jiān)測 BeforeWaiting (Loop即將進入休眠) 事件,這個Observer的回調(diào)函數(shù)是 _UIGestureRecognizerUpdateObserver(),其內(nèi)部會獲取所有剛被標記為待處理的 GestureRecognizer,并執(zhí)行GestureRecognizer的回調(diào)。
  6. 當有 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
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容