Runloop分析

RunLoop 參考:深入理解RunLoop

ibireme:《深入理解RunLoop》

Runloop 的概念

首先,讓一個線程隨時能處理事件,但是并不退出,這樣的模型通常稱作 Event Loop,如下:

funcation loop() {
    initialize();
    do {
        var message = get_next_message();
        process_message(message);
    } while (message != quit);
}

實現(xiàn)這種模型的關(guān)鍵點在于:如何管理事件/消息,如何讓線程在沒有消息處理時休眠以避免資源浪費,在消息到來時立刻被喚醒。

Runloop 實際上就是一個對象,這個對象管理了其所需要處理的事件和消息,并提供一個入口函數(shù)來執(zhí)行上面的 Event Loop 邏輯。線程執(zhí)行了這個函數(shù)后,就會一直處于這個函數(shù)內(nèi)部 “接收消息->等待->處理”的循環(huán)中,直到這個循環(huán)結(jié)束(如返回 quit 消息),函數(shù)返回。

iOS/MacOS 提供了兩個這樣的對象:NSRunLoopCFRunLoopRef

  • CFRunLoopRef 是在 CoreFoundation 框架內(nèi)的,它提供了純 CAPI,所有這些 API 都是線程安全的。
  • NSRunLoop 是基于 CFRunLoopRef 的封裝,提供了面向?qū)ο蟮?API,但這些 API 不是線程安全的。

RunLoop 與線程的關(guān)系

  1. RunLoop 是通過 p_thread 管理的。蘋果不允許直接創(chuàng)建 RunLoop,它提供了兩個自動獲取的方法:CFRunLoopGetCurrent()CFRunLoopGetMain()
  2. 線程和 RunLoop 是一一對應(yīng)的,其關(guān)系保存在一個全局的 Dictionary 里。線程剛創(chuàng)建時是沒有 RunLoop 的,如果不主動獲取,那它就會一直沒有。RunLoop 的創(chuàng)建是在第一次獲取時,銷毀發(fā)生在線程結(jié)束時。

RunLoop 對外的接口

CoreFoundation 里面關(guān)于 RunLoop 有五個類:

  • CFRunLoopRef
  • CFRunLoopSourceRef
  • CFRunLoopObserverRef
  • CFRunLoopTimerRef
  • CFRunLoopModeRef

其中, CFRunLoopModeRef 類沒有對外暴露,只是通過 CFRunLoopRef 的接口進行了封裝,他們關(guān)系如下:

RunLoop_0.png

一個 RunLoop 包含若干 Mode,每個 Mode 又包含若干 Source/Timer/Observer。每次調(diào)用 RunLoop 的主函數(shù)時,只能指定其中一個 Mode,這個 Mode 被稱為 CurrentMode。如果需要切換 Mode,只能退出 Loop,再重新指定一個 Mode 進入。這樣做主要是為了分割不同組的 Source/Timer/Observer,讓其互不影響。這也是為啥 ScrollView 滑動時,默認 Mode 下計時器停止的原因。

Source/Timer/Observer 被統(tǒng)稱為 mode item,一個 item 可以同時加入多個 mode。但一個 item 被重復(fù)加入同一個 mode 是不會有效果的。如果一個 mode 中一個 item 都沒有,則 RunLoop 會直接退出,不進入循環(huán)。

CFRunLoopSourceRef

是事件產(chǎn)生的地方。 Source 有兩個版本: Source0Source1

  • Source0 只包含一個回調(diào)(指針),它并不能主動觸發(fā)事件。使用時需要先調(diào)用 CFRunLoopSourceSignal(source),將這個 source 標(biāo)記為待處理, 然后手動調(diào)用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop ,讓其處理事件。
  • Source1 包含了一個 mach_port 和一個回調(diào)(指針),被用于通過內(nèi)核和其他線程相互發(fā)送消息。這種 Source 能主動喚醒 RunLoop 的線程。

CFRunLoopTimerRef

CFRunLoopTimerRef 是基于時間的觸發(fā)器,它和 NSTimertoll-free bridged 的,可以混用。其包含一個時間長度和回調(diào)(指針)。當(dāng)其加入到 RunLoop 時,RunLoop 會注冊對應(yīng)的時間點,當(dāng)時間點到時,RunLoop 被喚醒執(zhí)行那個回調(diào)。

CFRunLoopObserverRef

CFRunLoopObserverRef 是觀察者。 每個 Observer 都包含了一個回調(diào)(指針),當(dāng) RunLoop 狀態(tài)發(fā)生變化時,觀察者就能通過回調(diào)接收到這個變化。可以觀測的時間點有以下幾個:

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
};

RunLoop 的 Mode

CFRunLoopMode 和 CFRunLoop` 的結(jié)構(gòu)如下:

struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;  /* must have the run loop locked before locking this */
    CFStringRef _name;
    Boolean _stopped;
    char _padding[3];
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    CFMutableDictionaryRef _portToV1SourceMap;
    __CFPortSet _portSet;
    CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    dispatch_source_t _timerSource;
    dispatch_queue_t _queue;
    Boolean _timerFired; // set to true by the source when a timer has fired
    Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
    mach_port_t _timerPort;
    Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
    DWORD _msgQMask;
    void (*_msgPump)(void);
#endif
    uint64_t _timerSoftDeadline; /* TSR */
    uint64_t _timerHardDeadline; /* TSR */
};


struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;          /* locked for accessing mode list */
    __CFPort _wakeUpPort;           // used for CFRunLoopWakeUp 
    Boolean _unused;
    volatile _per_run_data *_perRunData;              // reset for runs of the run loop
    pthread_t _pthread;
    uint32_t _winthread;
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFTypeRef _counterpart;
};

篩選出比較關(guān)鍵的信息,大致如下:

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
    ...
};
  • 蘋果公開提供的 Mode 有兩個:kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和 UITrackingRunLoopMode,可以用這兩個 Mode Name 來操作其對應(yīng)的 Mode。
  • 可以自定義 Mode。RunLoop 內(nèi)部 Mode 只能增加,不能減少。

RunLoop 內(nèi)部邏輯

RunLoop 內(nèi)部的邏輯大致如下:

RunLoop_1.png

蘋果用 RunLoop 實現(xiàn)的功能

APP啟動時,系統(tǒng)默認注冊了5個Mode:

  1. kCFRunLoopDefaultMode: App的默認 Mode,通常主線程是在這個 Mode 下運行的。
  2. UITrackingRunLoopMode: 界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響。
  3. UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode,啟動完成后就不再使用。
  4. GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內(nèi)部 Mode,通常用不到。
  5. kCFRunLoopCommonModes: 這是一個占位的 Mode,沒有實際作用。

可以在 這里 看到更多的蘋果內(nèi)部的 Mode,但那些 Mode 在開發(fā)中就很難遇到了。

RunLoop 參考:iOS線下分享《RunLoop》by 孫源@sunnyxx

iOS線下分享《RunLoop》by 孫源@sunnyxx

RunLoop 機制

為什么要有 RunLoop:

  • 使程序一直運行并接受用戶輸入
  • 決定程序在何時應(yīng)該處理哪些 Event
  • 調(diào)用解耦(Message Quene
  • 節(jié)省 CPU 時間
RunLoop機制
  • RunLoop 與線程(Thread)一一綁定并非說是一個 Thread 只能對應(yīng)一個 RunLoop, 而是對應(yīng)一個在外層的 RunLoop,RunLoop 可以嵌套使用。
  • 1n 是通過數(shù)組結(jié)構(gòu)實現(xiàn)的。

CFRunLoopTimer

我們常用的 Timer 相關(guān)的,都是基于 CFRunLoopTimer 的封裝,如:

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti 
invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti 
invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

或者延遲執(zhí)行:

- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument
afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes;

或者屏幕刷新頻率 CADisplayLink

+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;
- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;

CFRunLoopSource

  • SourceRunLoop 的數(shù)據(jù)抽象類(Protocol)
  • RunLoop 定義了兩個 VersionSource,可以從堆棧信息中查看:
    • Source0: 處理 App 內(nèi)部事件、App 自己負責(zé)管理(觸發(fā)),如 UIEventCFSocket等。
    • Source1: 由 RunLoop 內(nèi)核管理,Mach Port 驅(qū)動,如CFMachPortCFMessagePort (都是官方文檔說的。。。)
    • 如果需要,可以選擇一種來實現(xiàn)自己的 Source。(這事兒知道就行了)

CFRunLoopObserver

/* Run Loop Observer Activities */
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
};
  • 向外部報告當(dāng)前 RunLoop 狀態(tài)的更改。
  • 框架中很多機制都由 CFRunLoopObserver 觸發(fā),如 CAAnimation。(其實這個也是猜測,并沒有實質(zhì)文檔說明)

Topic: CFRunLoopObserverAutorelease Pool 關(guān)系

UIKit 通過 RunLoopObserverRunLoop 循環(huán)過程中,對 Autorelease Pool 進行 PopPush 操作,將這次 Loop 中產(chǎn)生的 Autorelease 對象釋放。視頻中作者測試,是在兩次 Sleep 之間。

CFRunLoopMode

  • RunLoop 在同一段時間,只能,并且必須在一種特定的 ModeRun
  • 更換 Mode 時,當(dāng)前 Loop 會停止,然后重新啟動 Loop
  • ModeiOS App 滑動順暢的關(guān)鍵。
  • 可以定制自己的 Mode
  1. NSDefaultRunLoopMode: 默認狀態(tài),越是空閑時的狀態(tài)。
  2. UITrackingRunLoopMode: 滑動時的狀態(tài) ScrollView
  3. UIInitializationRunLoopMode: 私有(不可見,堆棧追蹤能看到,其他均為猜測)
  4. NSRunLoopCommonModes: 包含NSDefaultRunLoopModeUITrackingRunLoopMode 兩種狀態(tài)。:

Topic: UITrackingRunLoopModeTimer

  1. 這個方法,是將 Timer 加到默認的 NSDefaultRunLoopMode 模式下。
[NSTimer scheduledTimerWithTimeInterval:1
                                 target:self
                               selector:@selector(timeCount)
                               userInfo:nil
                                repeats:YES];
  1. 如果存在 ScrollView,滑動時,[NSRunLoop currentRunLoop].currentModeNSDefaultRunLoopMode 改變?yōu)?UITrackingRunLoopMode,此時,Timer 暫停,結(jié)束滑動后, Timer 繼續(xù)。如果想要滑動時 Timer 正常運行,則將 Timer 添加到 NSRunLoopCommonModes 模式下即可,即:
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

Topic: RunLoopMode 的切換

  • 滑動前:NSDefaultRunLoopMode
  • 滑動中:UITrackingRunLoopMode
  • 滑動結(jié)束后:NSDefaultRunLoopMode

RunLoopGCD

視頻中這一塊也是在討論,并沒有定論

  1. GCD 本身與 RunLoop 沒有關(guān)系。
  2. GCDdispatchmain queueblock 被分發(fā)到 main runloop 中執(zhí)行,dispatch_after 同理。

Runloop 的等待與喚醒

  • 指定用戶喚醒的 mach_port 端口。
  • 調(diào)用 mach_msg 監(jiān)聽喚醒端口,被喚醒前,系統(tǒng)內(nèi)核將這個線程掛起,停留在 mach_msg_trap 狀態(tài)。
  • 由另一個線程(或另一個進程中的某個線程)向內(nèi)核發(fā)送這個端口的 msg 后,trap 狀態(tài)被喚醒, Runloop 繼續(xù)開始干活。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 來源:『深入理解RunLoop』RunLoop 是 iOS 和OSX開發(fā)中非常基礎(chǔ)的一個概念,這篇文章將從CFRu...
    lever_xu閱讀 393評論 0 2
  • 轉(zhuǎn)自bireme,原地址:https://blog.ibireme.com/2015/05/18/runloop/...
    乜_啊_閱讀 1,427評論 0 5
  • ======================= 前言 RunLoop 是 iOS 和 OSX 開發(fā)中非常基礎(chǔ)的一個...
    i憬銘閱讀 900評論 0 4
  • RunLoop 的概念 一般來講,一個線程一次只能執(zhí)行一個任務(wù),執(zhí)行完成后線程就會退出。如果我們需要一個機制,讓線...
    Mirsiter_魏閱讀 629評論 0 2
  • 轉(zhuǎn)自http://blog.ibireme.com/2015/05/18/runloop 深入理解RunLoop ...
    飄金閱讀 1,002評論 0 4