詳解RunLoop之源碼分析

首發於 我的博客html

RunLoop是什麼

runloop 是什麼?Runloop 仍是比較顧名思義的一個東西,說白了就是一種循環,只不過它這種循環比較高級。通常的 while 循環會致使 CPU 進入忙等待狀態,而 Runloop 則是一種「閒」等待,這部分能夠類比 Linux 下的 epoll。當沒有事件時,Runloop 會進入休眠狀態,有事件發生時, Runloop 會去找對應的 Handler 處理事件。Runloop 可讓線程在須要作事的時候忙起來,不須要的話就讓線程休眠git

開始以前,先想一想這幾道面試題

  • runloop和線程的關係?
  • timer 與 runloop 的關係?
  • 程序中添加每3秒響應一次的NSTimer,當拖動tableview時timer可能沒法響應要怎麼解決?
  • runloop內部實現邏輯?
  • runloop 是怎麼響應用戶操做的, 具體流程是什麼樣的?
  • 說說runLoop的幾種狀態
  • runloop的mode做用是什麼?

源碼分析

回答問題以前,咱們先看源碼github

RunLoop 源碼 opensource.apple.com/tarballs/CF… 裏面數字最大的是最 新的,下載最新的 CF-1153.18.tar.gz(寫本文時候的最新版本)面試

查看源碼 中的bash

CFRunLoopRef CFRunLoopGetCurrent(void) {
      CHECK_FOR_FORK();
      CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
      if (rl) return rl;
      return _CFRunLoopGet0(pthread_self());
}
複製代碼

經過app

_CFRunLoopGet0 獲取的
複製代碼

進去查看作了什麼oop

loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
	if (!loop) {
	    CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
	    loop = newLoop;
	}
複製代碼

發現有這麼一個獲取線程的方法,也就是傳入一個線程做爲key,獲取一個loop,若是loop爲空,就以這個線程爲key建立runloop源碼分析

小結:ui

  • 每條線程都有惟一的一個與之對應的RunLoop對象
  • RunLoop保存在一個全局的Dictionary裏,線程做爲key,RunLoop做爲value
  • 線程剛建立時並無RunLoop對象,RunLoop會在第一次獲取它時建立
  • RunLoop會在線程結束時銷燬(下面分析這一條)

runloop的mode

接下來認識一下runloop的主要類 Core Foundation中關於RunLoop的5個類spa

CFRunLoopRef

CFRunLoopModeRef

CFRunLoopSourceRef

CFRunLoopTimerRef

CFRunLoopObserverRef
複製代碼

看一下runloop結構體

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;
    CFAbsoluteTime _runTime;
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
};
複製代碼

只保留主要的就剩下了

typedef struct __CFRunLoop * CFRunLoopRef;
 struct __CFRunLoop {
	 pthread_t _pthread;
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode; //當前模式
    CFMutableSetRef _modes; //全部的模式
  	CFMutableSetRef _modes;
};
複製代碼

理解爲CFRunLoopRef中包含有_modes,modes是由 CFRunLoopModeRef組成的集合 這些modes中,只有一種是當前模式,稱爲 _currentMode

接下來咱們看看runloopmode中究竟有什麼,一樣,只保留主要的,關鍵就是下面4個

總結起來就是

  • CFRunLoopModeRef表明RunLoop的運行模式
  • 一個RunLoop包含若干個Mode,每一個Mode又包含若干個Source0/Source1/Timer/Observer
  • RunLoop啓動時只能選擇其中一個Mode,做爲currentMode
  • 若是須要切換Mode,只能退出當前Loop,再從新選擇一個Mode進入
  • 不一樣組的Source0/Source1/Timer/Observer能分隔開來,互不影響
  • 若是Mode裏沒有任何Source0/Source1/Timer/Observer,RunLoop會立馬退出
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {

    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
};
複製代碼

咱們能夠理解爲,RunLoop中有許多模式,但當前運行的只有一種,一個圖來表示,就是

image.png

具體在某一種runloop中的運行邏輯,官方給出下圖

image.png

那麼,前面說的,_sources0、_sources一、_observers、_timers J究竟包含了什麼呢? 先用一張圖來總結一下,而後再詳細介紹

image.png

如上圖所示,_sources0 包含觸摸事件,和 performSelector:onThread: 跑一下代碼證實一下, 首先建立一個新項目,實現點擊事件,NSLog只是爲了打斷點

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"這個打印只是爲了打斷點");
}

@end

斷點暫停以後,輸入lldb指令 bt 以後如圖所示


複製代碼

image.png

從打印日誌來看 調用了__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ 這個SOURCE0方法 那麼接下來驗證一下performSelector

image.png
由上圖可知,performSelector 也是執行了source0

那咱們再看一下Timer

image.png
如上圖所示,此次是__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__

對於其餘幾種狀況,讀者課自行驗證。

用一幅圖來總結RunLoop的運行邏輯

image.png

源碼內部細節分析

要想分析源碼首先要知道入口在哪裏,由前面的斷點可知

image.png
入口爲 CFRunLoopRunSpecific 去源碼中找到以後發現有不少。只保留關鍵信息

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    // 通知Observers: 進入Loop
  __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    // 具體要作的事情
	result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    
     // 通知Observers: 退出Loop
	__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    
    return result;
}
複製代碼

那咱們繼續跟__CFRunLoopRun 看看作了什麼,發現裏面很長的東西,整理了一下,只保留關鍵代碼,以下

/* rl, rlm are locked on entrance and exit */
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    int32_t retVal = 0;
    do {
        // 通知Observers: 即將處理Timers
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        // 通知Observers: 即將處理Sources
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
        // 處理Blocks
        __CFRunLoopDoBlocks(rl, rlm);
        // 處理Sources0
        if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) {
            // 處理Blocks
            __CFRunLoopDoBlocks(rl, rlm);
        }
        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
        //判斷有無source1
        if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) { // 若是有source1 就跳轉到 handle_msg
            goto handle_msg;
        }
        // 通知Observers: 即將休眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        __CFRunLoopSetSleeping(rl);
        // 通知Observers: 結束休眠
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
    handle_msg:;
        
        if (被Timer喚醒) {
            // 處理Timers
            __CFRunLoopDoTimers(rl, rlm, mach_absolute_time())
        } else if (被gcd喚醒) {
            //處理GCD
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
        } else { //能來到這裏,就說明被Source1喚醒
            
            __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
            // 處理Blocks
            __CFRunLoopDoBlocks(rl, rlm);
            
            // 設置返回值,決定是否繼續循環
            if (sourceHandledThisLoop && stopAfterHandle) {
                retVal = kCFRunLoopRunHandledSource;
            } else if (timeout_context->termTSR < mach_absolute_time()) {
                retVal = kCFRunLoopRunTimedOut;
            } else if (__CFRunLoopIsStopped(rl)) {
                __CFRunLoopUnsetStopped(rl);
                retVal = kCFRunLoopRunStopped;
            } else if (rlm->_stopped) {
                rlm->_stopped = false;
                retVal = kCFRunLoopRunStopped;
            } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
                retVal = kCFRunLoopRunFinished;
            }
            voucher_mach_msg_revert(voucherState);
            os_release(voucherCopy);
        } while (0 == retVal);
        
        return retVal;
}


複製代碼

截圖下來的話,就是這樣的

image.png

###調用細節 前面說了大概的流程,那麼,具體怎麼調用的呢,lldb調試堆棧的時候,那些方法怎麼調用的呢?這裏以 __CFRunLoopDoTimers 爲例,看下源碼怎麼調用的

image.png
上圖可知,關鍵代碼是 CFRunLoopTimerRef 繼續查看 CFRunLoopTimerRef

image.png
關鍵代碼是__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ 看到這裏是否是對前面截圖中的調用堆棧更清晰了呢。 其餘幾種也都是相似的邏輯,就不贅述了。

目前已知的Mode有五種

目前已知的Mode有5種
kCFRunLoopDefaultMode:App的默認Mode,一般主線程是在這個Mode下運行

UITrackingRunLoopMode:界面跟蹤 Mode,用於 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其餘 Mode 影響

UIInitializationRunLoopMode:在剛啓動 App 時第進入的第一個 Mode,啓動完成後就再也不使用

GSEventReceiveRunLoopMode:接受系統事件的內部 Mode,一般用不到

kCFRunLoopCommonModes:這是一個佔位用的Mode,不是一種真正的Mode
複製代碼

RunLoop的狀態

/* Run Loop Observer Activities */
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
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};
複製代碼

常見的2種Mode

  • kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默認Mode,一般主線程是在這個Mode下運行

  • UITrackingRunLoopMode:界面跟蹤 Mode,用於 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其餘 Mode 影響

RunLoop 和 NSTimer

咱們知道,默認狀況下,NSTimer計時器,會被UIScrollView 打斷,會影響計時器的使用。緣由就是滾動時候,RunLoop切換到了UITrackingRunLoopMode模式下,但計時器在NSDefaultRunLoopMode下,因此就中止了。解決辦法就是設置NSRunLoopCommonModes。特別注意的是:

NSRunLoopCommonModes並非一個真的模式,它只是一個標記

本文參考資料:

RunLoop官方源碼

iOS底層原理

更多資料,歡迎關注我的公衆號

相關文章
相關標籤/搜索