runLoop瞭解

jialebaba發表於2019-02-26

什麼是runLoop

runLoop實際上是一個物件,這個物件管理了需要處理的事件和訊息,並提供了一個入口函式來執行Event Loop的邏輯。執行緒執行了這個入口函式後,函式內部會一直處在: 接收訊息 -> 等待 -> 處理,知道迴圈結束<傳去quit等>,函式返回

Event Loop

    function loop() {
        initialize();
        do {
            var message = get_next_message();
            process_message(message);
        } while (message != quit);
    }
複製程式碼

Event Loop的關鍵點在於:如何管理事件、訊息,如何讓執行緒在沒有出來訊息時休眠來減少資源佔用,在訊息到來時立即喚醒。

runLoop和執行緒的關係

蘋果不允許直接建立RunLoop,只提供了函式: CFRunLoopGenMain()、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的時候才會線上程中建立它,線上程銷燬時一起銷燬。

RunLoop的對外介面

在CoreFoundation中RunLoop有5個類:

CFRunLoopRef
CFRunLoopModeRef<未對外暴露,通過CFRunLoopRef封裝>
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserveRef
/*
    他們關係:RunLoop之下包含多個Mode,每個Mode中都包含若干個Source,Timer,Observe。呼叫RunLoop的主函式時,只能指定一個Mode,這個Mode被稱作CurrentMode,若想切換Mode只能退出Loop再重新指定一個Mode進入。
*/ 
複製程式碼

CFRunLoopSourceRef

CFRunLoopSourceRef:是事件產生的地方,有兩個版本:source0、source1.

  1. source0:只包含一個回撥(函式指標),他不能主動觸發事件。使用時,要先呼叫CFRunLoopSourceSignal(source)方法將source標記為待處理,後手動呼叫CFRunLoopSourcWeakUp(runloop)來喚醒Runloop來處理source。
  2. source1:包含一個mach_port和回撥。被用於通過核心和其他執行緒互相傳送訊息,能主動喚醒RunLoop的執行緒。

CFRunLoopTimerRef

CFRunLoopTimerRef:是基於事件的觸發器,他和NStimer是toll-free-bridged<能夠在Core Foundation和Foundation之間互換使用,這意味著同一資料型別即可以作為Core Foundation函式的引數,也可作為接收者向其傳送Objective-C訊息>。包含一個事件長度和回撥,當其加入到RunLoop時,RunLoop會註冊對應的時間點,當時間點到時,RunLoop會被喚醒以執行那個回撥。

CFRunLoopObserveRef

CFRunLoopObserveRef:觀察者,包含了一個回撥,,當RunLoop的狀態發生變化時,觀察者就能通過回撥接受到這個變化。可以觀測的時間點有以下幾個:

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
};
複製程式碼

CFRunLoopModeRef

CFRunLoopModeRef和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
    ...
};
複製程式碼

CommonModes:通過方法CFRunLoop對外暴露管理Mode介面:

// 新增mode<只能新增不能刪除>
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
// 用指定的Mode啟動,允許設定最大時間(假無線迴圈),執行完是否退出
CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle);
複製程式碼

Mode暴露管理的mode items介面:

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:

/*
1. kCFRunLoopDefaultMode:App的預設Mode,通常主執行緒是在這個Mode下執行
2. UITrackingRunLoopMode:介面跟蹤 Mode,用於 ScrollView 追蹤觸控滑動,保證介面滑動時不受其他 Mode 影響
3. UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode,啟動完成後就不再使用
4. GSEventReceiveRunLoopMode: 接受系統事件的內部 Mode,通常用不到
5. kCFRunLoopCommonModes: 這是一個佔位用的Mode,作為標記kCFRunLoopDefaultMode和UITrackingRunLoopMode用,並不是一種真正的Mode
*/
複製程式碼
/*使用*/

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    // [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];
    // 加入到RunLoop中才可以執行
    // 1. 把定時器新增到RunLoop中,並且選擇預設執行模式NSDefaultRunLoopMode = kCFRunLoopDefaultMode
    // [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    // 當textFiled滑動的時候,timer失效,停止滑動時,timer恢復
    // 原因:當textFiled滑動的時候,RunLoop的Mode會自動切換成UITrackingRunLoopMode模式,因此timer失效,當停止滑動,RunLoop又會切換回NSDefaultRunLoopMode模式,因此timer又會重新啟動了
 
    // 2. 當我們將timer新增到UITrackingRunLoopMode模式中,此時只有我們在滑動textField時timer才會執行
    // [[NSRunLoop mainRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
 
    // 3. 那個如何讓timer在兩個模式下都可以執行呢?
    // 3.1 在兩個模式下都新增timer 是可以的,但是timer新增了兩次,並不是同一個timer
    // 3.2 使用站位的執行模式 NSRunLoopCommonModes標記,凡是被打上NSRunLoopCommonModes標記的都可以執行,下面兩種模式被打上標籤
    //0 : <CFString 0x10b7fe210 [0x10a8c7a40]>{contents = "UITrackingRunLoopMode"}
    //2 : <CFString 0x10a8e85e0 [0x10a8c7a40]>{contents = "kCFRunLoopDefaultMode"}
    // 因此也就是說如果我們使用NSRunLoopCommonModes,timer可以在UITrackingRunLoopMode,kCFRunLoopDefaultMode兩種模式下執行
    [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    NSLog(@"%@",[NSRunLoop mainRunLoop]);
}
複製程式碼

RunLoop的內部呼叫

1. 通知Observer:即將進入Loop
2. 通知Observe:將要處理Timer
3. 處理Source0
4. 有Source1,跳到第96. 通知Observe:執行緒即將休眠
7. 休眠,等待喚醒-------------------------a.source0(port) b.timer c.外部
8. 通知Observer:執行緒剛被喚醒
9. 處理喚醒收到的訊息,跳回2
10. 通知Observer:即將退出loop
{
 /// 1. 通知Observers,即將進入RunLoop
    /// 此處有Observer會建立AutoreleasePool: _objc_autoreleasePoolPush();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
    do {
 
        /// 2. 通知 Observers: 即將觸發 Timer 回撥。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
        /// 3. 通知 Observers: 即將觸發 Source (非基於port的,Source0) 回撥。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
 
        /// 4. 觸發 Source0 (非基於port的) 回撥。
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
 
        /// 6. 通知Observers,即將進入休眠
        /// 此處有Observer釋放並新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
 
        /// 7. sleep to wait msg.
        mach_msg() -> mach_msg_trap();
        
 
        /// 8. 通知Observers,執行緒被喚醒
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
 
        /// 9. 如果是被Timer喚醒的,回撥Timer
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
 
        /// 9. 如果是被dispatch喚醒的,執行所有呼叫 dispatch_async 等方法放入main queue 的 block
        __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
 
        /// 9. 如果如果Runloop是被 Source1 (基於port的) 的事件喚醒了,處理這個事件
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
 
 
    } while (...);
 
    /// 10. 通知Observers,即將退出RunLoop
    /// 此處有Observer釋放AutoreleasePool: _objc_autoreleasePoolPop();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}

複製程式碼

RunLoop 底層

RunLoop的核心是基於mach_port(),其休眠呼叫的函式是mach_msg()。

應用

事件響應

手勢識別

定時器

PerformSelector

GCD

GCD中呼叫到dispatch_get_main_queue()方法時會呼叫到RunLoop的方法

網路請求