深入理解RunLoop

最近看了很多RunLoop的文章,看完很懵逼,決心整理一下,文章中大部分內容都是引用大神們的,但好歹對自己有個交代了,花了一個周天加幾個晚上熬夜完成的,有個產出還是很爽的,不多比比了,下面開始吧。

什么是RunLoop?

RunLoop是一個接收處理異步消息事件的循環,一個循環中:等待事件發生,然后將這個事件送到能處理它的地方。

RunLoop實際上是一個對象,這個對象在循環中用來處理程序運行過程中出現的各種事件(比如說觸摸事件、UI刷新事件、定時器事件、Selector事件)和消息,從而保持程序的持續運行;而且在沒有事件處理的時候,會進入睡眠模式,從而節省CPU資源,提高程序性能。

Event Loop模型偽代碼

int main(int argc, char * argv[]) {    
     //程序一直運行狀態
     while (AppIsRunning) {
          //睡眠狀態,等待喚醒事件
          id whoWakesMe = SleepForWakingU  p();
          //得到喚醒事件
          id event = GetEvent(whoWakesMe);
          //開始處理事件
          HandleEvent(event);
     }
     return 0;
}
Screen Shot 2018-03-25 at 3.41.32 PM.png
  • mach kernel屬于蘋果內核,RunLoop依靠它實現了休眠和喚醒而避免了CPU的空轉。
  • Runloop是基于pthread進行管理的,pthread是基于c的跨平臺多線程操作底層API。它是mach thread的上層封裝(可以參見Kernel Programming Guide),和NSThread一一對應(而NSThread是一套面向對象的API,所以在iOS開發中我們也幾乎不用直接使用pthread)。
Screen Shot 2018-03-25 at 4.28.26 PM.png

RunLoop的組成

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共包含5個類,但公開的只有Source、Timer、Observer相關的三個類。

CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef

image

CFRunLoopSourceRef
source是RunLoop的數據源(輸入源)的抽象類(protocol),Source有兩個版本:Source0 和 Source1

  • source0:只包含了一個回調(函數指針),使用時,你需要先調用 CFRunLoopSourceSignal(source),將這個 Source 標記為待處理,然后手動調用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件。處理App內部事件,App自己負責管理(出發),如UIEvent(Touch事件等,GS發起到RunLoop運行再到事件回調到UI)、CFSocketRef。
  • Source1:由RunLoop和內核管理,由mach_port驅動(特指port-based事件),如CFMachPort、CFMessagePort、NSSocketPort。特別要注意一下Mach port的概念,它是一個輕量級的進程間通訊的方式,可以理解為它是一個通訊通道,假如同時有幾個進程都掛在這個通道上,那么其它進程向這個通道發送消息后,這些掛在這個通道上的進程都可以收到相應的消息。這個Port的概念非常重要,因為它是RunLoop休眠和被喚醒的關鍵,它是RunLoop與系統內核進行消息通訊的窗口。

CFRunLoopTimerRef 是基于時間的觸發器,它和 NSTimer 是toll-free bridged 的,可以混用(底層基于使用mk_timer實現)。它受RunLoop的Mode影響(GCD的定時器不受RunLoop的Mode影響),當其加入到 RunLoop 時,RunLoop會注冊對應的時間點,當時間點到時,RunLoop會被喚醒以執行那個回調。如果線程阻塞或者不在這個Mode下,觸發點將不會執行,一直等到下一個周期時間點觸發。

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;

這里要提一句的是,timer和source1(也就是基于port的source)可以反復使用,比如timer設置為repeat,port可以持續接收消息,而source0在一次觸發后就會被runloop移除。

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

RunLoop主要處理以下6類事件

static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();

RunLoop的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都是無法添加的。那為何我們又可以這么用呢

[[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中。

在項目中最常用的就是設置NSTimer的Mode,比較簡單這里就不說了。

RunLoop運行機制

image

當你調用 CFRunLoopRun() 時,線程就會一直停留在這個循環里;直到超時或被手動停止,該函數才會返回。每次線程運行RunLoop都會自動處理之前未處理的消息,并且將消息發送給觀察者,讓事件得到執行。RunLoop運行時首先根據modeName找到對應mode,如果mode里沒有source/timer/observer,直接返回。

/// 用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的掛起和喚醒

RunLoop的掛起

RunLoop的掛起是通過_CFRunLoopServiceMachPort —call—> mach_msg —call—> mach_msg_trap這個調用順序來告訴內核RunLoop監聽哪個mach_port(上面提到的消息通道),然后等待事件的發生(等待與InputSource、Timer描述內容相關的事件),這樣內核就把RunLoop掛起了,即RunLoop休眠了。

RunLoop的喚醒

這接種情況下會被喚醒

  1. 存在Source0被標記為待處理,系統調用CFRunLoopWakeUp喚醒線程處理事件
  2. 定時器時間到了
  3. RunLoop自身的超時時間到了
  4. RunLoop外部調用者喚醒

當RunLoop被掛起后,如果之前監聽的事件發生了,由另一個線程(或另一個進程中的某個線程)向內核發送這個mach_port的msg后,trap狀態被喚醒,RunLoop繼續運行

處理事件

  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)

RunLoop 的底層實現

關于這個大家可以看ibireme的深入理解RunLoop一文,我這里選擇一些覺得比較重要又不是那么難懂的。
Mach消息發送機制看這篇文章Mach消息發送機制

為了實現消息的發送和接收,mach_msg() 函數實際上是調用了一個 Mach 陷阱 (trap),即函數mach_msg_trap(),陷阱這個概念在 Mach 中等同于系統調用。當你在用戶態調用 mach_msg_trap() 時會觸發陷阱機制,切換到內核態;內核態中內核實現的 mach_msg() 函數會完成實際的工作,如下圖:

image

RunLoop 的核心就是一個 mach_msg() (見上面代碼的第7步),RunLoop 調用這個函數去接收消息,如果沒有別人發送 port 消息過來,內核會將線程置于等待狀態。例如你在模擬器里跑起一個 iOS 的 App,然后在 App 靜止時點擊暫停,你會看到主線程調用棧是停留在 mach_msg_trap() 這個地方。

RunLoop和線程

RunLoop和線程是息息相關的,我們知道線程的作用是用來執行特定的一個或多個任務,但是在默認情況下,線程執行完之后就會退出,就不能再執行任務了。這時我們就需要采用一種方式來讓線程能夠處理任務,并不退出。所以,我們就有了RunLoop。

iOS開發中能遇到兩個線程對象: pthread_t和NSThread,pthread_t和NSThread 是一一對應的。比如,你可以通過 pthread_main_thread_np()或 [NSThread mainThread]來獲取主線程;也可以通過pthread_self()或[NSThread currentThread]來獲取當前線程。CFRunLoop 是基于 pthread 來管理的。

線程與RunLoop是一一對應的關系(對應關系保存在一個全局的Dictionary里),線程創建之后是沒有RunLoop的(主線程除外),RunLoop的創建是發生在第一次獲取時,銷毀則是在線程結束的時候。只能在當前線程中操作當前線程的RunLoop,而不能去操作其他線程的RunLoop。

蘋果不允許直接創建RunLoop,但是可以通過[NSRunLoop currentRunLoop]或者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時,則需要手動創建和運行RunLoop(尤其是在子線程中, 主線程中的Main RunLoop除外),我看到別人舉了這么個例子,很有意思

調用[NSTimer scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:]帶有schedule的方法簇來啟動Timer.

此方法會創建Timer并把Timer放到當前線程的RunLoop中,隨后RunLoop會在Timer設定的時間點回調Timer綁定的selector或Invocation。但是,在主線程和子線程中調用此方法的效果是有差異的,即在主線程中調用scheduledTimer方法時timer可以在設定的時間點觸發,但是在子線程里則不能觸發。這是因為子線程中沒有創建RunLoop且更沒有啟動RunLoop,而主線程中的RunLoop默認是創建好的且一直運行著。所以,子線程中需要像下面這樣調用。

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doTimer) userInfo:nil repeats:NO];
  [[NSRunLoop currentRunLoop] run];
});

那為什么下面這樣調用同樣不會觸發Timer呢?
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  [[NSRunLoop currentRunLoop] run];
  [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doTimer) userInfo:nil repeats:NO];
});  

我的分析是:scheduledTimerWithTimeInterval內部在向RunLoop傳遞Timer時是調用與線程實例相關的單例方法[NSRunLoop currentRunLoop]來獲取RunLoop實例的,即RunLoop實例不存在就創建一個與當前線程相關的RunLoop并把Timer傳遞到RunLoop中,存在則直接傳Timer到RunLoop中即可。而在RunLoop開始運行后再向其傳遞Timer時,由于dispatch_async代碼塊里的兩行代碼是順序執行,[[NSRunLoop currentRunLoop] run]是一個沒有結束時間的RunLoop,無法執行到“[NSTimer scheduledTimerWithTimeInterval:…”這一行代碼,Timer也就沒有被加到當前RunLoop中,所以更不會觸發Timer了。

蘋果用 RunLoop 實現的功能

AutoreleasePool

App啟動之后,系統啟動主線程并創建了RunLoop,在main thread中注冊了兩個observer,回調都是_wrapRunLoopWithAutoreleasePoolHandler()

第一個Observer監視的事件

  1. 即將進入Loop(kCFRunLoopEntry),其回調內會調用 _objc_autoreleasePoolPush() 創建自動釋放池。其order是-2147483647,優先級最高,保證創建釋放池發生在其他所有回調之前。

第二個Observer監視了兩個事件

  1. 準備進入休眠(kCFRunLoopBeforeWaiting),此時調用 _objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 來釋放舊的池并創建新的池。

  2. 即將退出Loop(kCFRunLoopExit)此時調用 _objc_autoreleasePoolPop()釋放自動釋放池。這個 Observer的order是2147483647,確保池子釋放在所有回調之后。

我們知道AutoRelease對象是被AutoReleasePool管理的,那么AutoRelease對象在什么時候被回收呢?

第一種情況:在我們自己寫的for循環或線程體里,我們都習慣用AutoReleasePool來管理一些臨時變量的autorelease,使得在for循環或線程結束后回收AutoReleasePool的時候來回收AutoRelease臨時變量。

另一種情況:我們在主線程里創建了一些AutoRelease對象,這些對象可不能指望在回收Main AutoReleasePool時才被回收,因為App一直運行的過程中Main AutoReleasePool是不會被回收的。那么這種AutoRelease對象的回收就依賴Main RunLoop的運行狀態,Main RunLoop的Observer會在Main RunLoop結束休眠被喚醒時(kCFRunLoopAfterWaiting狀態)通知UIKit,UIKit收到這一通知后就會調用_CFAutorleasePoolPop方法來回收主線程中的所有AutoRelease對象。

在主線程中執行代碼一般都是寫在事件回調或Timer回調中的,這些回調都被加入了main thread的自動釋放池中,所以在ARC模式下我們不用關心對象什么時候釋放,也不用去創建和管理pool。(如果事件不在主線程中要注意創建自動釋放池,否則可能會出現內存泄漏)。

NSTimer(timer觸發)

上文說到了CFRunLoopTimerRef,其實NSTimer的原型就是CFRunLoopTimerRef。一個Timer注冊 RunLoop 之后,RunLoop 會為這個Timer的重復時間點注冊好事件。有兩點需要注意:

  1. 但是需要注意的是RunLoop為了節省資源,并不會在非常準確的時間點回調這個Timer。Timer 有個屬性叫做 Tolerance (寬容度),標示了當時間點到后,容許有多少最大誤差。這個誤差默認為0,我們可以手動設置這個誤差。文檔最后還強調了,為了防止時間點偏移,系統有權力給這個屬性設置一個值無論你設置的值是多少,即使RunLoop 模式正確,當前線程并不阻塞,系統依然可能會在 NSTimer 上加上很小的的容差。
  2. 我們在哪個線程調用 NSTimer 就必須在哪個線程終止

在RunLoop的Mode中也有說到,NSTimer使用的時候注意Mode,比如我之前開發時候用NSTimer寫一個Banner圖片輪播框架,如果不設置Timer的Mode為commonModes那么在滑動TableView的時候Banner就停止輪播

DispatchQueue.global().async {
    // 非主線程不能使用 Timer.scheduledTimer進行初始化
//                    self.timer = Timer.scheduledTimer(timeInterval: 6.0, target: self, selector: #selector(TurnPlayerView.didTurnPlay), userInfo: nil, repeats: false)
    
    if #available(iOS 10.0, *) {
        self.timer = Timer(timeInterval: 6.0, repeats: true, block: { (timer) in
            self.setContentOffset(CGPoint(x: self.frame.width*2, y: self.contentOffset.y), animated: true)
        })
    } else {
        // Fallback on earlier versions
    }
    
    
    RunLoop.main.add(self.timer!, forMode: RunLoopMode.commonModes)
}

和GCD的關系

  1. RunLoop底層用到GCD
  2. RunLoop與GCD并沒有直接關系,但當GCD使用到main_queue時才有關系,如下:
//實驗GCD Timer 與 Runloop的關系,只有當dispatch_get_main_queue時才與RunLoop有關系
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    NSLog(@"GCD Timer...");
});

當調用 dispatch_async(dispatch_get_main_queue(), block) 時,libDispatch 會向主線程的 RunLoop 發送消息,RunLoop會被喚醒,并從消息中取得這個 block,并在回調 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里執行這個 block。但這個邏輯僅限于 dispatch 到主線程,dispatch 到其他線程仍然是由 libDispatch 處理的。同理,GCD的dispatch_after在dispatch到main_queue時的timer機制才與RunLoop相關。

PerformSelecter

NSObject的performSelecter:afterDelay: 實際上其內部會創建一個 Timer 并添加到當前線程的 RunLoop 中。所以如果當前線程沒有 RunLoop,則這個方法會失效。
NSObject的performSelector:onThread: 實際上其會創建一個 Timer 加到對應的線程去,同樣的,如果對應線程沒有 RunLoop 該方法也會失效。
其實這種方式有種說法也叫創建常駐線程(內存),AFNetworking也用到這種技法。舉個例子,如果把RunLoop去掉,那么test方法就不會執行。


class SecondViewController: UIViewController {
    
    var thread: Thread!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = UIColor.red
        thread = Thread.init(target: self, selector: #selector(SecondViewController.run), object: nil)
        thread.start()
        
    }
    
    @objc func run() {
        
        print("run -- ")
        RunLoop.current.add(Port(), forMode: .defaultRunLoopMode)
        RunLoop.current.run()
    }
    
    @objc func test() {
        print("test --  \(Thread.current)")
    }
    
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        //        self.test()
        
        self.perform(#selector(SecondViewController.test), on: thread, with: nil, waitUntilDone: false)
        
    }
    
    
}

網絡請求

iOS中的網絡請求接口自下而上有這么幾層

Screen Shot 2018-03-26 at 2.13.00 PM.png

其中CFSocket和CFNetwork偏底層,早些時候比較知名的網絡框架AFNetworking是基于NSURLConnection編寫的,iOS7之后新增了NSURLSession,NSURLSession的底層仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 線程),之后AFNetworking和Alamofire就是基于它封裝的了。

image

通常使用 NSURLConnection 時,會傳入一個 Delegate,當調用了 [connection start] 后,這個 Delegate 就會不停收到事件回調。實際上,start 這個函數的內部會獲取 CurrentRunLoop,然后在其中的 DefaultMode 添加了4個 Source0 (即需要手動觸發的Source)。CFMultiplexerSource 是負責各種 Delegate 回調的,CFHTTPCookieStorage 是處理各種 Cookie 的。

開始網絡傳輸時,NSURLConnection 創建了兩個新線程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。

其中 CFSocket 線程是處理底層 socket 連接的,NSURLConnectionLoader中的RunLoop通過一些基于mach port的Source1接收來自底層CFSocket的通知。當收到通知后,其會在合適的時機向CFMultiplexerSource等Source0發送通知,同時喚醒Delegate線程的RunLoop來讓其處理這些通知。CFMultiplexerSource會在Delegate線程的RunLoop對Delegate執行實際的回調。

事件響應

蘋果注冊了一個 Source1 (基于 mach port 的) 用來接收系統事件,其回調函數為 __IOHIDEventSystemClientQueueCallback()。

當一個硬件事件(觸摸/鎖屏/搖晃等)發生后,首先由 IOKit.framework 生成一個 IOHIDEvent 事件并由 SpringBoard 接收。SpringBoard 只接收按鍵(鎖屏/靜音等),觸摸,加速,接近傳感器等幾種 Event,隨后用 mach port 轉發給需要的App進程。

觸摸事件其實是Source1接收系統事件后在回調 __IOHIDEventSystemClientQueueCallback()內觸發的 Source0,Source0 再觸發的 _UIApplicationHandleEventQueue()。source0一定是要喚醒runloop及時響應并執行的,如果runloop此時在休眠等待系統的 mach_msg事件,那么就會通過source1來喚醒runloop執行。

_UIApplicationHandleEventQueue() 會把 IOHIDEvent 處理并包裝成 UIEvent 進行處理或分發,其中包括識別 UIGesture/處理屏幕旋轉/發送給 UIWindow 等。

image

手勢識別

當上面的 _UIApplicationHandleEventQueue() 識別了一個手勢時,其首先會調用 Cancel 將當前的 touchesBegin/Move/End 系列回調打斷。隨后系統將對應的 UIGestureRecognizer 標記為待處理。

蘋果注冊了一個 Observer 監測 BeforeWaiting (Loop即將進入休眠) 事件,這個Observer的回調函數是 _UIGestureRecognizerUpdateObserver(),其內部會獲取所有剛被標記為待處理的 GestureRecognizer,并執行GestureRecognizer的回調。

當有 UIGestureRecognizer 的變化(創建/銷毀/狀態改變)時,這個回調都會進行相應處理。

UI更新

Core Animation 在 RunLoop 中注冊了一個 Observer 監聽 BeforeWaiting(即將進入休眠) 和 Exit (即將退出Loop) 事件 。當在操作 UI 時,比如改變了 Frame、更新了 UIView/CALayer 的層次時,或者手動調用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,這個 UIView/CALayer 就被標記為待處理,并被提交到一個全局的容器去。當Oberver監聽的事件到來時,回調執行函數中會遍歷所有待處理的UIView/CAlayer 以執行實際的繪制和調整,并更新 UI 界面。

如果此處有動畫,通過 DisplayLink 穩定的刷新機制會不斷的喚醒runloop,使得不斷的有機會觸發observer回調,從而根據時間來不斷更新這個動畫的屬性值并繪制出來。

函數內部的調用棧

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
    QuartzCore:CA::Transaction::observer_callback:
        CA::Transaction::commit();
            CA::Context::commit_transaction();
                CA::Layer::layout_and_display_if_needed();
                    CA::Layer::layout_if_needed();
                          [CALayer layoutSublayers];
                          [UIView layoutSubviews];
                    CA::Layer::display_if_needed();
                          [CALayer display];
                          [UIView drawRect];

繪圖和動畫有兩種處理的方式:CPU(中央處理器)和GPU(圖形處理器)
CPU: CPU 中計算顯示內容,比如視圖的創建、布局計算、圖片解碼、文本繪制等
GPU: GPU 進行變換、合成、渲染.

關于CADisplayLink的描述有兩種

CADisplayLink 是一個和屏幕刷新率一致的定時器(但實際實現原理更復雜,和 NSTimer 并不一樣,其內部實際是操作了一個 Source)。如果在兩次屏幕刷新之間執行了一個長任務,那其中就會有一幀被跳過去(和 NSTimer 相似),造成界面卡頓的感覺。在快速滑動TableView時,即使一幀的卡頓也會讓用戶有所察覺。

CADisplayLink是一個執行頻率(fps)和屏幕刷新相同(可以修改preferredFramesPerSecond改變刷新頻率)的定時器,它也需要加入到RunLoop才能執行。與NSTimer類似,CADisplayLink同樣是基于CFRunloopTimerRef實現,底層使用mk_timer(可以比較加入到RunLoop前后RunLoop中timer的變化)。和NSTimer相比它精度更高(盡管NSTimer也可以修改精度),不過和NStimer類似的是如果遇到大任務它仍然存在丟幀現象。通常情況下CADisaplayLink用于構建幀動畫,看起來相對更加流暢,而NSTimer則有更廣泛的用處。

不管怎么樣CADisplayLink和NSTimer是有很大不同的,詳情可以參考這篇文章CADisplayLink

ibireme根據CADisplayLink的特性寫了個FPS指示器YYFPSLabel,代碼非常少
原理是這樣的:既然CADisplayLink可以以屏幕刷新的頻率調用指定selector,而且iOS系統中正常的屏幕刷新率為60Hz(60次每秒),所以使用 CADisplayLink 的 timestamp 屬性,配合 timer 的執行次數計算得出FPS數

參考文章
深入理解RunLoop
iOS 事件處理機制與圖像渲染過程
RunLoop學習筆記(一) 基本原理介紹
iOS刨根問底-深入理解RunLoop
【iOS程序啟動與運轉】- RunLoop個人小結
RunLoop的前世今生
Runloop知識樹

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 前言 RunLoop是iOS和OSX開發中非常基礎的一個概念,這篇文章將從CFRunLoop的源碼入手,介紹Run...
    暮年古稀ZC閱讀 2,306評論 1 19
  • 轉載:http://www.cocoachina.com/ios/20150601/11970.html RunL...
    Gatling閱讀 1,471評論 0 13
  • RunLoop 的概念 一般來講,一個線程一次只能執行一個任務,執行完成后線程就會退出。如果我們需要一個機制,讓線...
    Mirsiter_魏閱讀 629評論 0 2
  • 原文地址:http://blog.ibireme.com/2015/05/18/runloop/ RunLoop ...
    大餅炒雞蛋閱讀 1,181評論 0 6
  • 生活陷入了新一輪的空虛,像從前,我怕是又陷入了想要睡覺的漩渦。毫無節制的吃,毫無節制的表達揮霍自己的健康和話語。 ...
    亞茹_我是阿茹閱讀 88評論 0 1