iOS:RunLoop詳解

RunLoop 本質:安全

RunLoop 本質上是一個運行循環,其做用是保持線程的生命,防止線程被銷燬,平常開發中無處不在,爲了感覺到 RunLoop 的存在,舉個簡單的例子,咱們都知道,APP程序的入口在main.m裏面:bash

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}
複製代碼

接下來咱們試着感覺一下 RunLoop,點擊查看 UIApplicationMain,其返回值是一個 int 值:網絡

// If nil is specified for principalClassName, the value for NSPrincipalClass from the Info.plist is used. If there is no
// NSPrincipalClass key specified, the UIApplication class is used. The delegate class will be instantiated using init.
UIKIT_EXTERN int UIApplicationMain(int argc, char * _Nullable argv[_Nonnull], NSString * _Nullable principalClassName, NSString * _Nullable delegateClassName);

複製代碼

咱們在 main 函數裏面用一個 int 變量去接收它,而後在返回值先後添加輸出語句:函數

int main(int argc, char * argv[]) {
    @autoreleasepool {
        NSLog(@"1 %@", [NSThread currentThread]);
        
        int a = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
        
        NSLog(@"2 %@", [NSThread currentThread]);
        
        return a;
    }
}
複製代碼

運行程序,看一下輸出: oop

image.png

咱們看到,只執行了第一個輸出,而且當前線程是主線程,第二個輸出語句並無執行,沒有執行的緣由,有兩種可能,第一種:UIApplicationMain 內部阻塞;第二種:UIApplicationMain 函數內部開啓了一個死循環;可是一旦程序發生阻塞,APP 就沒法使用了,那麼說得通的可能性就是第二種,這個函數內部開啓了死循環,就是 RunLoop 循環,它的做用,就是保持主線程的生命,而且幫咱們監聽一些事件,好比觸摸、時鐘、網絡等;ui

Runloop 的兩種形式:spa

OC: Foundation ——> NSRunLoop線程

// NSRunLoop獲取RunLoop
[NSRunloop currentRunloop];  // 得到當前線程的RunLoop的方法
[NSRunloop  mainRunloop];   // 得到主線程的RunLoop的方法   
複製代碼

C: CoreFoudation ——> CFRunLoopRef指針

// CFRunLoopRef獲取RunLoop
CFRunloopGetCurrent();  // 得到當前線程的RunLoop的方法  
CFRunloopGetMain();   // 得到主線程的RunLoop的方法  
複製代碼

NSRunLoop 是對 CFRunLoopRef 的進一步封裝,而且 CFRunLoopRef 是線程安全的,而這一點 NSRunLoop 並不能保證。code

RunLoop 與線程的關係:

每一條線程都有一個與之對應的 RunLoop 對象; RunLoop 保存在全局的字典內,是以線程爲 key,RunLoop 爲 value; RunLoop 的建立是發生在第一次獲取時(懶加載, 不獲取是不會建立的),RunLoop 的銷燬是發生在線程結束時,只能在一個線程的內部獲取其 RunLoop(主線程除外)。

RunLoop 對外的接口:

在 CoreFoundation 裏面關於 RunLoop 有5個類:

typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef;    // CFRunLoopRef

typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoopSource * CFRunLoopSourceRef;   // CFRunLoopSourceRef

typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoopObserver * CFRunLoopObserverRef;  // CFRunLoopObserverRef

typedef struct CF_BRIDGED_MUTABLE_TYPE(NSTimer) __CFRunLoopTimer * CFRunLoopTimerRef;   // CFRunLoopTimerRef

CFRunLoopModeRef
複製代碼

其中 CFRunLoopModeRef 類並無對外暴露,只是經過 CFRunLoopRef 的接口進行了封裝:

image.png

一個 RunLoop 包含若干個 Mode,每一個 Mode 又包含若干個 Source / Timer / Observer,每次調用 RunLoop 的主函數時,只能指定其中一個 Mode,這個 Mode 被稱做 CurrentMode,若是須要切換 Mode,只能退出 Loop,再從新指定一個 Mode 進入。這樣作主要是爲了分隔開不一樣組的 Source / Timer / Observer,讓其互不影響。

CFRunLoopSourceRef 是事件產生的地方。Source有兩個版本:Source0 和 Source1。 • Source0 只包含了一個回調(函數指針),它並不能主動觸發事件。使用時,你須要先調用 CFRunLoopSourceSignal(source),將這個 Source 標記爲待處理,而後手動調用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件。 • Source1 包含了一個 mach_port 和一個回調(函數指針),被用於經過內核和其餘線程相互發送消息。這種 Source 能主動喚醒 RunLoop 的線程,其原理在下面會講到。

CFRunLoopTimerRef 是基於時間的觸發器,它和 NSTimer 是 toll-free bridged 的,能夠混用。其包含一個時間長度和一個回調(函數指針)。當其加入到 RunLoop 時,RunLoop會註冊對應的時間點,當時間點到時,RunLoop 會被喚醒以執行那個回調。

CFRunLoopObserverRef 是觀察者,每一個 Observer 都包含了一個回調(函數指針),當 RunLoop 的狀態發生變化時,觀察者就能經過回調接受到這個變化,RunLoop 的狀態以下:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),  // 即將進入RunLoop
    kCFRunLoopBeforeTimers = (1UL << 1),  // 即將處理Timer
    kCFRunLoopBeforeSources = (1UL << 2),   // 即將處理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5),  // 即將進入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),  // 剛從休眠中喚醒
    kCFRunLoopExit = (1UL << 7),  // 即將退出RunLoop
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};
複製代碼

上面的 Source / Timer / Observer 被統稱爲 mode item,一個 item 能夠被同時加入多個 Mode。但一個 item 被重複加入同一個 Mode 時是不會有效果的。若是一個 Mode 中一個 item 都沒有,則 RunLoop 會直接退出,不進入循環。

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
    ...
};
複製代碼

常見的2種 Mode:

kCFRunloopDefaultMode  //程序的默認Mode
UITrackingRunloopMode  //界面追蹤Mode
複製代碼

不一樣 Mode 對應的事件類型都不相同,例如咱們平時使用的 Timer:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0f target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
    // timerWithTimeInterval:target:selector:userInfo:repeats: 建立的 Timer 須要手動添加到RunLoop中才能運行
    // 兩個 Mode 同時添加,在兩個模式下都會運行
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];  // 默認模式下運行,但當觸摸事件發生時,就會中止
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];  // 有觸摸事件時纔會運行,可是時間結束後就會中止
    
    // 添加到 NSRunLoopCommonModes 中,在兩個模式下都會運行
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}


- (void)timerMethod {
    NSLog(@"%@", [NSThread currentThread]);
}

複製代碼

經過這種方式建立的 Timer 自己是不會運行的,只有將 Timer 添加到 RunLoop 中才會運行,咱們先將 Mode 設置爲 NSDefaultRunLoopMode,Timer 會運行,而且打印輸出, 這個時候咱們往界面中添加交互控件,例如添加一個ScrollView,當咱們滑動這個控件時, Timer 就會中止,響應完觸摸事件又會恢復運行,這是由於 RunLoop 會優先響應 UITrackingRunloopMode 的事件,相反,若是咱們將 Mode 設置爲 UITrackingRunloopMode,那正常狀況下 Timer 不會運行,當有 UI 事件的時候,纔會運行;這和咱們的初衷相背離,咱們但願在任何狀況下,Timer 都能運行,這個時候,咱們就要將 Timer 同時添加到 NSDefaultRunLoopMod 和 UITrackingRunloopMode 兩個模式下,NSRunLoopCommonModes 就是爲咱們解決這個問題。

注: kCFRunloopCommonModes 默認包括了 kCFRunloopDefaultMode 和 UITrackingRunloopMode; kCFRunloopCommonModes 並非一個真正的模式,只是一個標記,而 kCFRunloopDefaultMode 和 UITrackingRunloopMode 的模式是真正意義的模式,timer 在設置了 kCFRunloopCommonModes 標記的模式下都能運行,而 kCFRunloopDefaultMode 和 UITrackingRunloopMode 這兩個模式都是 kCFRunloopCommonModes 標記的。

RunLoop 運行邏輯:

image.png

// 用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 就是這樣一個函數,其內部是一個 do-while 循環。當你調用 CFRunLoopRun() 時,線程就會一直停留在這個循環裏,直到超時或被手動中止,該函數纔會返回。

相關文章
相關標籤/搜索