NSRunLoop底層原理詳解

NSRunLoop

  • 原文博客地址: NSRunLoop底層原理詳解
  • 正常狀況下, 一個線程執行完, 程序就會當即退出, 好比一個命令行項目
  • NSRunLoopiOS中的消息處理機制,執行完某個事件後線程不會退出,而是進入休眠狀態,當再次監測到須要出發事件時,線程激活,繼續處理事件,處理完成後再次進入休眠
  • 這種時間運行循環, 相似於一個while循環
  • 默認狀況下, 不須要咱們手動建立RunLoop, 由於cocoa框架爲咱們建立了一個默認的RunLoop
  • RunLoop的主要做用
    • 保持程序的持續運行
    • 處理App中的各類事件(手勢、定時器、Selector等)
    • 節省CPU資源、提升程序性能:該作任務的時候作任務,沒事幹的時候休息
  • RunLoop和線程的關係
    • 每條線程都有惟一的一個與之對應的RunLoop對象
    • RunLoop保存在一個全局的Dictionary裏, 線程做爲key, RunLoop做爲value
    • 線程剛建立時並無RunLoop對象, RunLoop會在第一次獲取線程時建立
    • RunLoop會在線程結束時銷燬
    • 主線程的RunLoop已經自動獲取(建立), 子線程默認沒有開啓RunLoop

RunLoop對象

  • iOS開發中RunLoop有兩套API框架, 分別是
    • FoundationNSRunLoop
    • Core FoundationCFRunLoopRef
  • CFRunLoopRef是基於C語言的開源框架, 有興趣的能夠到源碼地址下載源碼, 不過沒有C語言功底的只怕很難看懂
  • NSRunLoop是對CFRunLoopRef的有一層封裝, 是OC語法的框架
  • 簡單使用, 獲取RunLoop對象
// 獲取當前線程的RunLoop
[NSRunLoop currentRunLoop];
// 獲取主線程的RunLoop
[NSRunLoop mainRunLoop];
    
// 獲取主線程的RunLoop
CFRunLoopGetMain();
// 獲取當前線程的RunLoop
CFRunLoopGetCurrent();
複製代碼

RunLoop相關的類

  • 由於NSRunLoop是不開源的, 可是CFRunLoopRef倒是開源的, 從源碼地址下載CFRunLoopRef的源碼
  • 在源碼中能夠看到, 在Core FoundationCFRunLoopRef有如下5個相關的類
    • CFRunLoopRef
    • CFRunLoopModeRef
    • CFRunLoopSourceRef
    • CFRunLoopTimerRef
    • CFRunLoopObserverRef

CFRunLoopRef

CFRunLoopRef對象的主要核心代碼以下html

typedef struct __CFRunLoop * CFRunLoopRef;

struct __CFRunLoop {
    // 線程對象
    pthread_t _pthread;
    // 無序集合
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    // 當前mode
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
};
複製代碼

主要屬性介紹前端

  • CFMutableSetRef是一個無序的集合, 在上面的代碼中存儲的都是CFRunLoopModeRef對象
  • 其中_modes存儲的是全部的mode對象
  • _currentMode是指當前的mode

CFRunLoopModeRef

typedef struct __CFRunLoopMode *CFRunLoopModeRef;

struct __CFRunLoopMode {
    // mode名稱
    CFStringRef _name;
    Boolean _stopped;
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
};
複製代碼
  • _name: 該__CFRunLoopMode的名稱
  • _sources0_sources1: 一個無序集合, 存儲的都是CFRunLoopSourceRef對象
  • _observers: 一個有序集合數組,存儲的都是CFRunLoopObserverRef對象
  • _timers: 一個有序集合數組,存儲的都是CFRunLoopTimerRef對象
  • 從這裏咱們能夠看出以上幾個類之間的關係, 大概可以下圖所示

image

  • CFRunLoopModeRef表明RunLoop的運行模式
  • 一個RunLoop只能對應一個線程, 卻包含若干個Mode,每一個Mode又包含若干個Source0/Source1/Timer/Observer
  • RunLoop啓動時只能選擇其中一個Mode,做爲currentMode一樣只能有一個
  • 若是須要切換Mode,只能退出當前Loop,再從新選擇一個Mode進入, 不一樣組的Source0/Source1/Timer/Observer能分隔開來,互不影響
  • 若是Mode裏沒有任何Source0/Source1/Timer/ObserverRunLoop會立馬退出
  • 如下是系統默認的集中mode
FOUNDATION_EXPORT NSRunLoopMode const NSDefaultRunLoopMode;
FOUNDATION_EXPORT NSRunLoopMode const NSRunLoopCommonModes;

UIKIT_EXTERN NSRunLoopMode const UITrackingRunLoopMode;

CF_EXPORT const CFRunLoopMode kCFRunLoopDefaultMode;
CF_EXPORT const CFRunLoopMode kCFRunLoopCommonModes;
複製代碼
  • kCFRunLoopDefaultModeNSDefaultRunLoopMode):App的默認Mode,一般主線程是在這個Mode下運行
  • UITrackingRunLoopMode:界面跟蹤Mode,用於ScrollView追蹤觸摸滑動,保證界面滑動時不受其餘Mode影響
  • kCFRunLoopCommonModesNSRunLoopCommonModes): 並非某一種特定的mode, 而是通用模式, 包括kCFRunLoopDefaultModeUITrackingRunLoopMode

CFRunLoopObserverRef

CFRunLoopObserverRef是觀察者,可以監聽RunLoop全部的狀態改變。 能夠監聽的時間點有以下幾種:數組

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

在主線程監聽全部的狀態bash

// 建立observer
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry:
            NSLog(@"kCFRunLoopEntry");
            break;
            case kCFRunLoopBeforeTimers:
            NSLog(@"kCFRunLoopBeforeTimers");
            break;
            case kCFRunLoopBeforeSources:
            NSLog(@"kCFRunLoopBeforeSources");
            break;
            case kCFRunLoopBeforeWaiting:
            NSLog(@"kCFRunLoopBeforeWaiting");
            break;
            case kCFRunLoopAfterWaiting:
            NSLog(@"kCFRunLoopAfterWaiting");
            break;
            case kCFRunLoopExit:
            NSLog(@"kCFRunLoopExit");
            break;
            default:
            break;
        }
    });
    // 吧observer添加到RunLoop中
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    // 釋放
    CFRelease(observer);
複製代碼

RunLoop消息類型

image

從上圖能夠看出消息類型大概能夠分出兩種, 第一種類型又能夠細分爲三種, 這三種都是異步執行的微信

Port

監聽程序的mach portsports能夠簡單的理解爲:內核經過port這種方式將信息發送,而mach則監聽內核發來的port信息,而後將其整理,打包發給runloop數據結構

Customer

由開發人員本身發送, 蘋果也提供了一個CFRunLoopSource來幫助處理, 簡單介紹核心實:架構

  1. 定義輸入源(數據結構)
  2. 將輸入源添加到runloop,那麼這樣就有了接受者,即爲R1
  3. 協調輸入源的客戶端(單獨線程),專門監聽消息,而後將消息打包成runloop可以處理的樣式,即第一步定義的輸入源。它相似Mach的功能

Selector Sources

NSObject類提供了不少方法供咱們使用,這些方法是添加到runloop的,因此若是沒有開啓runloop的話,不會運行app

/// 主線程
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
	
/// 指定線程
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait;

/// 針對當前線程, 延遲調用
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes;
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;

/// 取消,在當前線程,和上面兩個方法對應
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(nullable id)anArgument;
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget;
複製代碼
  • 上面提到的前四個方法是在指定線程運行aSelector, 通常狀況下aSelector會添加到指定線程的runloop
  • 若是調用線程和指定線程爲同一線程,且wait參數設爲YES,那麼aSelector會直接在指定線程運行,再也不添加到runloop;
  • 由於wait參數設爲YES, 意味着要等待aSelector執行完成以後纔回去執行後面的邏輯

RunLoop運行邏輯

根據蘋果在文檔裏的說明,RunLoop 內部的邏輯大體以下:框架

image

未查看RunLoop的執行流程, 咱們能夠新建一個項目, 並簡單加一個觸發事件, 以下所示異步

image

  • 如圖所示, 添加一個簡單的觸發事件, 並加上斷點, 在打印區域輸入bt命令後, 就能看到完整的執行流程了
  • 從下往上查看, 所執行的相關函數大概流程是:
  • UIApplicationMain
  • CFRunLoopRunSpecific
  • __CFRunLoopRun
  • __CFRunLoopDoSources0
  • 最後就是[UIResponder touchesBegan:withEvent:]觸發函數了
  • 下面的事情就是找到源碼, 依次查看所執行的函數了
  • 在源碼中找到CFRunLoop.c文件, 搜索CFRunLoopRunSpecific方法, 就是核心代碼了, 一塊兒來看看吧
  • 刪除其餘不相關的代碼, 核心代碼大概以下
/// RunLoop的實現, 大概在文件的2622行
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { {
    
    /// 首先根據modeName找到對應mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    
    /// 1. 通知 Observers: RunLoop 即將進入 loop。
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    
    /// __CFRunLoopRun中具體要作的事情
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    
    /// 11. 通知 Observers: RunLoop 即將退出。
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    
    return result;
}


// __CFRunLoopRun的實現, 進入loop, 大概在文件的2304行
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {

    int32_t retVal = 0;
    do {

        // 2. 通知 Observers: RunLoop 即將觸發 Timer 回調。
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        
        // 3. 通知 Observers: RunLoop 即將觸發 Source0 (非port) 回調。
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);

        // 4. 處理block
        __CFRunLoopDoBlocks(rl, rlm);

        // 5. 處理Source0
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        // 若是處理Source0的結果是rrue
        if (sourceHandledThisLoop) {
            // 再次處理block
            __CFRunLoopDoBlocks(rl, rlm);
        }

        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);

        // 6. 若是有Source1 (基於port) 處於ready狀態,直接處理這個Source1而後跳轉去處理消息。
        if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) {
            // 若是有Source1, 就跳轉到handle_msg
            goto handle_msg;
        }

        // 7. 通知 Observers: RunLoop 的線程即將進入休眠(sleep)
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        __CFRunLoopSetSleeping(rl);
	
        // 7. 調用mach_msg等待接受mach_port的消息。線程將進入休眠, 等待別的消息來喚醒當前線程
        // 一個基於 port 的Source 的事件。
        // 一個 Timer 到時間了
        // RunLoop 自身的超時時間到了
        // 被其餘什麼調用者手動喚醒
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
        
        
        __CFRunLoopUnsetSleeping(rl);
        // 8. 通知Observers: 結束休眠, RunLoop的線程剛剛被喚醒了
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

        
    // 收到消息,處理消息。
    handle_msg:;
        if (/* 被timer喚醒 */) {
            // 01. 處理Timer
            __CFRunLoopDoTimers(rl, rlm, mach_absolute_time())
        } else if (/* 被gcd喚醒 */) {
            // 02. 處理gcd
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
        } else {  // 被Source1喚醒
            // 處理Source1
            sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls) || sourceHandledThisLoop;
	    }
        
        // 9. 處理Blocks
        __CFRunLoopDoBlocks(rl, rlm);
        
        // 10. 設置返回值, 根據不一樣的結果, 處理不一樣操做
        if (sourceHandledThisLoop && stopAfterHandle) {
            // 進入loop時參數說處理完事件就返回。
            retVal = kCFRunLoopRunHandledSource;
        } else if (timeout_context->termTSR < mach_absolute_time()) {
            // 超出傳入參數標記的超時時間了
            retVal = kCFRunLoopRunTimedOut;
        } else if (__CFRunLoopIsStopped(rl)) {
             // 被外部調用者強制中止了
            retVal = kCFRunLoopRunStopped;
        } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
            // source/timer/observer一個都沒有了
            retVal = kCFRunLoopRunFinished;
        }

        // 若是沒超時,mode裏沒空,loop也沒被中止,那繼續loop。
    } while (0 == retVal);

    return retVal;
}
複製代碼

從上面的代碼能夠看到RunLoop其內部是一個do-while循環; 當你調用CFRunLoopRun()時,線程就會一直停留在這個循環裏;直到超時或被手動中止,該函數纔會返回

RunLoop的底層實現

  • 從上面代碼能夠看到,RunLoop的核心是基於mach port的,其進入休眠時調用的函數是mach_msg()
  • Mach自己提供的API很是有限,並且蘋果也不鼓勵使用MachAPI
  • 可是這些API很是基礎,若是沒有這些API的話,其餘任何工做都沒法實施
  • Mach中,全部的東西都是經過本身的對象實現的,進程、線程和虛擬內存都被稱爲」對象」
  • 和其餘架構不一樣,Mach的對象間不能直接調用,只能經過消息傳遞的方式實現對象間的通訊。
  • "消息"是Mach中最基礎的概念,消息在兩個端口 (port) 之間傳遞,這就是MachIPC (進程間通訊) 的核心。

Mach的消息定義是在<mach/message.h>頭文件的,很簡單:

typedef struct {
  mach_msg_header_t header;
  mach_msg_body_t body;
} mach_msg_base_t;

typedef struct {
  mach_msg_bits_t msgh_bits;
  mach_msg_size_t msgh_size;
  mach_port_t msgh_remote_port;
  mach_port_t msgh_local_port;
  mach_port_name_t msgh_voucher_port;
  mach_msg_id_t msgh_id;
} mach_msg_header_t;
複製代碼
  • 一條Mach消息實際上就是一個二進制數據包 (BLOB),其頭部定義了當前端口local_port和目標端口remote_port
  • 發送和接受消息是經過同一個API進行的,其option標記了消息傳遞的方向:
mach_msg_return_t mach_msg(
	mach_msg_header_t *msg,
	mach_msg_option_t option,
	mach_msg_size_t send_size,
	mach_msg_size_t rcv_size,
	mach_port_name_t rcv_name,
	mach_msg_timeout_t timeout,
	mach_port_name_t notify
);
複製代碼
  • 爲了實現消息的發送和接收,mach_msg()函數其實是調用了一個Mach陷阱(trap),即函數mach_msg_trap(),陷阱這個概念在Mach中等同於系統調用
  • 當你在用戶態調用mach_msg_trap()時會觸發陷阱機制,切換到內核態;內核態中內核實現的mach_msg()函數會完成實際的工做
  • 內核態中的mach_msg(), 若是沒有消息就讓線程休眠,有消息就喚醒線程

image

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

NSRunLoop應用實踐

NSTimer問題

解決NSTimer在滑動時中止工做的問題

  • 上文有說到CFRunLoopMode主要使用的通常有三種Mode
  • DefaultModeApp平時所處的狀態,TrackingRunLoopMode是追蹤ScrollView滑動時的狀態
  • 當你建立一個Timer並加到DefaultMode時,Timer會獲得重複回調,但此時滑動一個TableView時,RunLoop會將mode切換爲TrackingRunLoopMode,這時Timer就不會被回調,而且也不會影響到滑動操做
  • 下面我來看一下這個例子
#import "ViewController.h"

@interface ViewController ()
// 在xib中添加一個可滾動的UITextView
@property (weak, nonatomic) IBOutlet UITextView *textView;
@property (assign, nonatomic) NSInteger timerCount;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 添加一個定時器
    [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerClick) userInfo:nil repeats:YES];
}
    
- (void)timerClick {
    NSLog(@"--------%ld", (long)self.timerCount++);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"touchesBegan");
}
@end
複製代碼
  • 上面代碼中scheduledTimerWithTimeInterval方式添加的NSTimer會默認被添加到DefaultMode
  • 當程序運行的時候回正常執行定時器的方法
  • 當咱們正常滾動UITextView的時候, 從打印結果能夠看到, 定時器中止執行了, 結束滾動UITextView的時候, 定時器方法會繼續執行
  • 輸出結果以下所示
2019-08-20 21:46:01.986843+0800 RunLoop[86811:3484205] --------0
2019-08-20 21:46:02.986723+0800 RunLoop[86811:3484205] --------1
2019-08-20 21:46:03.986040+0800 RunLoop[86811:3484205] --------2
2019-08-20 21:46:04.986274+0800 RunLoop[86811:3484205] --------3
2019-08-20 21:46:05.272525+0800 RunLoop[86811:3484205] touchesBegan
2019-08-20 21:46:12.291035+0800 RunLoop[86811:3484205] --------4
2019-08-20 21:46:12.986318+0800 RunLoop[86811:3484205] --------5
2019-08-20 21:46:13.986197+0800 RunLoop[86811:3484205] --------6
2019-08-20 21:46:14.986735+0800 RunLoop[86811:3484205] --------7
複製代碼
  • 有時你須要一個Timer,在兩個Mode中都能獲得回調
  • 一種辦法就是將這個Timer分別加入這兩個Mode
  • 另外一種方式,就是將Timer加入到頂層的RunLoopcommonModeItems
  • commonModeItemsRunLoop自動更新到全部具備Common屬性的Mode裏去
  • CommonModes並非一個真的模式,它只是一個標記
NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
    NSLog(@"--------%ld", (long)self.timerCount++);
}];

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
複製代碼

歡迎您掃一掃下面的微信公衆號,訂閱個人博客!

微信公衆號
相關文章
相關標籤/搜索