iOS底層學習 - 深刻RunLoop

RunLoop這個名詞對於iOS開發來講應該是一個聽膩了的詞彙,並且只知其一不知其二,本篇章就來再深刻複習一下RunLoopphp

RunLoop簡介

什麼是RunLoop

通常來說,一個線程一次只能執行一個任務,執行完成後線程就會退出。若是咱們須要一個機制,讓線程能隨時處理事件但並不退出,這種模型一般被稱做 Event Loop。 Event Loop 在不少系統和框架裏都有實現,好比 Node.js 的事件處理,好比 Windows 程序的消息循環,再好比 OSX/iOS 裏的 RunLoophtml

實現這種模型的關鍵點在於:管理事件/消息,讓線程在沒有處理消息時休眠以免資源佔用、在有消息到來時馬上被喚醒。數組

RunLoop做用

  1. 保持程序持續運行:程序一啓動就會開一個主線程,主線程一開起來就會跑一個主線程對應的RunLoop,RunLoop保證主線程不會被銷燬,也就保證了程序的持續運行
  2. 處理App中的各類事件(好比:觸摸事件,定時器事件,Selector事件等)
  3. 節省CPU資源,提升程序性能:程序運行起來時,當什麼操做都沒有作的時候,RunLoop就告訴CPU,如今沒有事情作,我要去休息,這時CPU就會將其資源釋放出來去作其餘的事情,當有事情作的時候RunLoop就會立馬起來去作事情

咱們先經過API內一張圖片來簡單看一下RunLoop內部運行原理bash

爲何使用RunLoop

瞭解了RunLoop的做用,那麼在蘋果系統中,爲何使用RunLoop呢?主要有一下幾點服務器

  1. 使程序一直運行,並接受用戶輸入
  2. 決定程序在什麼時候處理應該處理哪些Event
  3. 調用解耦(Message Queue): 好比一次滑屏事件,可能會觸發多條消息,因此必須有一個相似Message Queue的模塊去處理來解耦,造成一個隊列來依次處理,這樣用戶的調用方和處理方實現了徹底解耦
  4. 節省CPU的時間和效率

RunLoop底層原理

咱們仍是查看源碼來進行探究網絡

RunLoop代碼層級

  1. NSRunloop:最上層的NSRunloop層其實是對C語言實現的CFRunloop的一個封裝,實際上它沒幹什麼事,好比CFRunloop有一個過時時間是double類型,NSRunloop把它變味了NSDate類型;
  2. CFRunloop:這是真正幹事的一層,源代碼是開源的,而且是跨平臺的;
  3. 系統層:底層實現用到了GCD,mach kernel是蘋果的內核,好比runloop的睡眠和喚醒就是用mach kernel來實現的。

RunLoop入口

在OC代碼中,Runloop是由系統默認開啓的,就再main函數中,會開啓主線程和RunLoop。若是沒有Runloop,那麼main函數執行完畢後,程序就退出了,這說明在UIApplicationMain函數中,開啓了一個和主線程相關的RunLoop,致使UIApplicationMain不會返回,一直在運行中,也就保證了程序的持續運行。數據結構

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

接着咱們查看源碼,發現CFRunLoopRun的底層實現結構也很是簡單,就是一個do...while循環,咱們能夠把RunLoop當作一個死循環。若是沒有RunLoop,UIApplicationMain函數執行完畢以後將直接返回,也就沒有程序持續運行一說了。app

void CFRunLoopRun(void) {    /* DOES CALLOUT */
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
複製代碼

RunLoop與線程的關係

首先,iOS 開發中能遇到兩個線程對象: pthread_tNSThread。過去蘋果有份文檔標明瞭 NSThread只是 pthread_t 的封裝,但那份文檔已經失效了,如今它們也有可能都是直接包裝自最底層的 mach thread。蘋果並無提供這兩個對象相互轉換的接口,但無論怎麼樣,能夠確定的是 pthread_tNSThread 是一一對應的。好比,你能夠經過 pthread_main_thread_np()[NSThread mainThread] 來獲取主線程;也能夠經過 pthread_self()[NSThread currentThread] 來獲取當前線程。CFRunLoop 是基於 pthread 來管理的。框架

蘋果不容許直接建立 RunLoop,它只提供了兩個自動獲取的函數:CFRunLoopGetMain()CFRunLoopGetCurrent()。 這兩個函數內部的邏輯大概是下面這樣:iphone

✅// 得到當前線程的RunLoop對象,內部調用_CFRunLoopGet0函數
CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}

✅// 查看_CFRunLoopGet0方法
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    ✅// 若是爲空則t設置爲主線程
    if (pthread_equal(t, kNilPthreadT)) {
    t = pthread_main_thread_np();
    }
    __CFLock(&loopsLock);
    ✅// 若是不存在runloop,則建立
    if (!__CFRunLoops) {
        __CFUnlock(&loopsLock);
    CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
    ✅// 根據傳入的主線程獲取主線程對應的RunLoop
    CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
    ✅// 保存主線程 將主線程-key和RunLoop-Value保存到字典中
    CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
    if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
        CFRelease(dict);
    }
    CFRelease(mainLoop);
        __CFLock(&loopsLock);
    }
    
    ✅// 從字典裏面拿,將線程做爲key從字典裏獲取一個loop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    
    ✅// 若是loop爲空,則建立一個新的loop,因此runloop會在第一次獲取的時候建立
    if (!loop) {  
    CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
    loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    
    ✅// 建立好以後,以線程爲key runloop爲value,一對一存儲在字典中,下次獲取的時候,則直接返回字典內的runloop
    if (!loop) { 
        CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
        loop = newLoop;
    }
        // do not release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
        __CFUnlock(&loopsLock);
    ✅//線程結束是銷燬loop
    CFRelease(newLoop);
    }
    ✅// 若是傳入線程和當前線程相同
    if (pthread_equal(t, pthread_self())) {
        ✅// 註冊一個回調,當線程銷燬時,順便也銷燬對應的RunLoop
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;
}
複製代碼

經過源碼分析能夠看出,線程和RunLoop之間是一一對應的,其關係是保存在一個Dictionary字典裏。因此咱們建立子線程RunLoop時,只需在子線程中獲取當前線程的RunLoop對象便可[NSRunLoop currentRunLoop];。若是不獲取,那子線程就不會建立與之相關聯的RunLoop,而且只能在一個線程的內部獲取其RunLoop。

當經過調用[NSRunLoop currentRunLoop];方法獲取RunLoop時,會先看一下字典裏有沒有子線程對應的RunLoop,若是有則直接返回RunLoop,若是沒有則會建立一個,並將與之對應的子線程存入字典中。當線程結束時,RunLoop會被銷燬。

總結一下Runloop與線程的關係

  1. 每條線程都有惟一的一個與之對應的RunLoop對象
  2. RunLoop保存在一個全局的Dictionary裏,線程做爲key,RunLoop做爲value
  3. 主線程的RunLoop已經自動建立好了,子線程的RunLoop須要主動建立
  4. RunLoop在第一次獲取時建立,在線程結束時銷燬

RunLoop底層結構

CFRunLoopRef

經過源碼咱們找到__CFRunLoop結構體

typedef struct __CFRunLoop * CFRunLoopRef;

struct __CFRunLoop
{
    // CoreFoundation 中的 runtime 基礎信息
    CFRuntimeBase _base;
    // 針對獲取 mode 列表操做的鎖
    pthread_mutex_t _lock; /* locked for accessing mode list */
    // 喚醒端口
    __CFPort _wakeUpPort;  // used for CFRunLoopWakeUp
    // 是否使用過
    Boolean _unused;
    // runloop 運行會重置的一個數據結構
    volatile _per_run_data *_perRunData; // reset for runs of the run loop
    // runloop 所對應線程
    pthread_t _pthread;
    uint32_t _winthread;
    // 存放 common mode 的集合
    CFMutableSetRef _commonModes;
    // 存放 common mode item 的集合
    CFMutableSetRef _commonModeItems;
    // runloop 當前所在 mode
    CFRunLoopModeRef _currentMode;
    // 存放 mode 的集合
    CFMutableSetRef _modes;
    
    // runloop 內部 block 鏈表表頭指針
    struct _block_item *_blocks_head;
    // runloop 內部 block 鏈表表尾指針
    struct _block_item *_blocks_tail;
    // 運行時間點
    CFAbsoluteTime _runTime;
    // 休眠時間點
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
};

// 每次 RunLoop 運行後會重置
typedef struct _per_run_data
{
    uint32_t a;
    uint32_t b;
    uint32_t stopped;   // runloop 是否中止
    uint32_t ignoreWakeUps; // runloop 是否已喚醒
} _per_run_data;

// 鏈表節點
struct _block_item
{
    // 指向下一個 _block_item
    struct _block_item *_next;
    // 要麼是 string 類型,要麼是集合類型,也就是說一個 block 可能對應單個或多個 mode
    CFTypeRef _mode; // CFString or CFSet
    // 存放的真正要執行的 block
    void (^_block)(void);
};

};
複製代碼

經過查看RunLoop的底層結構,咱們發現了RunLoop也是一個結構體對象,其中有幾個主要的變量:

  • CFRunLoopModeRef _currentMode:runloop 當前所在 mode
  • CFMutableSetRef _modes:存放 mode 的集合

經過上述變量,咱們能夠知道:

  • RunLoop能夠有多個mode對象
  • Runloop在同一時間只能且必須在某一種特定的Mode下面Run,更換Mode時,必需要中止當前的Loop,而後重啓新的Loop,重啓的意思是退出當前的while循環,而後從新設置一個新的while

CFRunLoopModeRef

CFRunLoopModeRef 實際上是指向__CFRunLoopMode結構體的指針,__CFRunLoopMode結構體源碼以下

typedef struct __CFRunLoopMode *CFRunLoopModeRef;

struct __CFRunLoopMode
{
    // CoreFoundation 中的 runtime 基礎信息
    CFRuntimeBase _base;
    // 互斥鎖,加鎖前須要 runloop 先加鎖
    pthread_mutex_t _lock; /* must have the run loop locked before locking this */
    // mode 的名稱
    CFStringRef _name;
    // mode 是否中止
    Boolean _stopped;
    char _padding[3];
    // source0
    CFMutableSetRef _sources0;
    // source1
    CFMutableSetRef _sources1;
    // observers
    CFMutableArrayRef _observers;
    // timers
    CFMutableArrayRef _timers;
    CFMutableDictionaryRef _portToV1SourceMap;
    // port 的集合
    __CFPortSet _portSet;
    // observer 的 mask
    CFIndex _observerMask;
    // 若是定義了 GCD 定時器
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    // GCD 定時器
    dispatch_source_t _timerSource;
    // 隊列
    dispatch_queue_t _queue;
    // 當 GCD 定時器觸發時設置爲 true
    Boolean _timerFired; // set to true by the source when a timer has fired
    Boolean _dispatchTimerArmed;
#endif
// 若是使用 MK_TIMER
#if USE_MK_TIMER_TOO
    // MK_TIMER 的 port
    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 */
};

複製代碼

咱們發現,一個CFRunLoopModeRef也包含不少變量,主要有_sources0,_sources0兩個集合和_observers,_timers兩個數組。

這說明一個mode能夠包含多種items模式

CFRunLoopSourceRef

CFRunLoopSourceRef是事件源(輸入源)。經過源碼能夠發現,其分爲source0source1兩個。

  • source0:處理App內部事件,App本身負責管理(觸發),如UIEventCFSocket等;
  • source1:由Runloop和內核管理,mach port驅動,如CFMachPort(輕量級的進程間通訊的方式,NSPort就是對它的封裝,還有Runloop的睡眠和喚醒就是經過它來作的),CFMessagePort
typedef struct __CFRunLoopSource * CFRunLoopSourceRef;

struct __CFRunLoopSource
{
    // CoreFoundation 中的 runtime 基礎信息
    CFRuntimeBase _base;
    uint32_t _bits;
    // 互斥鎖
    pthread_mutex_t _lock;
    // source 的優先級,值爲小,優先級越高
    CFIndex _order; /* immutable */
    // runloop 集合
    CFMutableBagRef _runLoops;
    // 一個聯合體,說明 source 要麼爲 source0,要麼爲 source1
    union {
        CFRunLoopSourceContext version0;  /* immutable, except invalidation */
        CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
    } _context;
};

typedef struct {
    CFIndex version;
    // source 的信息
    void *  info;
    const void *(*retain)(const void *info);
    void    (*release)(const void *info);
    CFStringRef (*copyDescription)(const void *info);
    // 判斷 source 相等的函數
    Boolean (*equal)(const void *info1, const void *info2);
    CFHashCode  (*hash)(const void *info);
    void    (*schedule)(void *info, CFRunLoopRef rl, CFStringRef mode);
    void    (*cancel)(void *info, CFRunLoopRef rl, CFStringRef mode);
    // source 要執行的任務塊
    void    (*perform)(void *info);
} CFRunLoopSourceContext;

複製代碼

CFRunLoopObserverRef

CFRunLoopObserverRef是觀察者,每一個Observer都包含了一個回調(函數指針),當RunLoop的狀態發生變化時,觀察者就能經過回調接受到這個變化。主要是用來向外界報告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
    kCFRunLoopAllActivities = 0x0FFFFFFFU // 全部事件
};
複製代碼
typedef struct __CFRunLoopObserver * CFRunLoopObserverRef;

struct __CFRunLoopObserver
{
    // CoreFoundation 中的 runtime 基礎信息
    CFRuntimeBase _base;
    // 互斥鎖
    pthread_mutex_t _lock;
    // observer 對應的 runloop
    CFRunLoopRef _runLoop;
    // observer 觀察了多少個 runloop
    CFIndex _rlCount;
    CFOptionFlags _activities;          /* immutable */
    // observer 優先級
    CFIndex _order;                     /* immutable */
    // observer 回調函數
    CFRunLoopObserverCallBack _callout; /* immutable */
    // observer 上下文
    CFRunLoopObserverContext _context;  /* immutable, except invalidation */
};

typedef struct {
    CFIndex	version;
    void *	info;
    const void *(*retain)(const void *info);
    void	(*release)(const void *info);
    CFStringRef	(*copyDescription)(const void *info);
} CFRunLoopObserverContext;

typedef void (*CFRunLoopObserverCallBack)(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info);

複製代碼

CFRunLoopTimerRef

CFRunLoopTimerRef 是基於時間的觸發器,它和NSTimer是toll-free bridged的,能夠混用。其包含一個時間長度和一個回調(函數指針)。

當其加入到RunLoop時,RunLoop會註冊對應的時間點,當時間點到時,RunLoop會被喚醒以執行那個回調。

總結一下關於RunLoop的結構

  1. RunLoop本質也是一個結構體對象
  2. RunloopMode是指的一個事件循環必須在某種模式下跑,系統會預約義幾個模式。一個Runloop有多個Mode;
  3. CFRunloopSourceCFRunloopTimerCFRunloopObserver這些元素是在Mode裏面的,Mode與這些元素的對應關係也是1對多的。可是必須至少有一個Source或者Timer,由於若是Mode爲空,RunLoop運行到空模式不會進行空轉,就會馬上退出。
  4. CFRunloopSource分爲source0(處理用戶事件)和source1(處理內核事件)
  5. CFRunloopObserver是監聽和通知Runloop狀態

RunLoop的Mode

RunLoop 有五種運行模式,其中常見的有1.2兩種。

1. kCFRunLoopDefaultMode:App的默認Mode,一般主線程是在這個Mode下運行
2. UITrackingRunLoopMode:界面跟蹤 Mode,用於 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其餘 Mode 影響
3. UIInitializationRunLoopMode: 在剛啓動 App 時第進入的第一個 Mode,啓動完成後就再也不使用,會切換到kCFRunLoopDefaultMode
4. GSEventReceiveRunLoopMode: 接受系統事件的內部 Mode,一般用不到
5. kCFRunLoopCommonModes: 這是一個佔位用的Mode,做爲標記kCFRunLoopDefaultMode和UITrackingRunLoopMode用,並非一種真正的Mode 
複製代碼

Mode間的切換

咱們平時在開發中必定遇到過,當咱們使用NSTimer每一段時間執行一些事情時滑動UIScrollView,NSTimer就會暫停,當咱們中止滑動之後,NSTimer又會從新恢復的狀況,這是因爲RunloopMode必須在同一個模式下跑。

主線程的 RunLoop 裏有兩個預置的 Mode:kCFRunLoopDefaultModeUITrackingRunLoopMode。這兩個 Mode 都已經被標記爲」Common」屬性。DefaultMode 是 App 平時所處的狀態,TrackingRunLoopMode是追蹤 ScrollView 滑動時的狀態。當你建立一個 Timer 並加到 DefaultMode 時,Timer 會獲得重複回調,但此時滑動一個TableView時,RunLoop 會將 mode 切換爲 TrackingRunLoopMode,這時 Timer 就不會被回調,而且也不會影響到滑動操做。

有時你須要一個 Timer,在兩個 Mode 中都能獲得回調,一種辦法就是將這個 Timer 分別加入這兩個 Mode。還有一種方式,就是將 Timer 加入到頂層的 RunLoop 的 「commonModeItems」。」commonModeItems」 被 RunLoop 自動更新到全部具備」Common」屬性的 Mode 裏去。

一個 Mode 能夠將本身標記爲」Common」屬性(經過將其 ModeName 添加到 RunLoop 的 「commonModes」 中)。每當 RunLoop 的內容發生變化時,RunLoop 都會自動將 _commonModeItems 裏的 Source/Observer/Timer 同步到具備 「Common」 標記的全部Mode裏。

RunLoop啓動邏輯

咱們知道在main函數啓動時,會有Runloop的用DefaultMode默認啓動和使用指定Mode進行啓動,相關的源碼以下,能夠發現,其核心邏輯都是調用了CFRunLoopRunSpecific函數

/// 用DefaultMode啓動
void CFRunLoopRun(void) {   /* DOES CALLOUT */
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
/// 用指定的Mode啓動,並容許設置RunLoop的超時時間
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
複製代碼

CFRunLoopRunSpecific

接着咱們查看CFRunLoopRunSpecific函數,根據其代碼,主要總結爲如下幾個步驟:

  • 從 runloop 中查找給定的 mode
  • 將查找到的 mode 賦值到 runloop 的 _curentMode,也就是說在這 runloop 完成了 mode 的切換
  • 調用核心函數 __CFRunLoopRun
  • 若是註冊了 observer,則通知runloop的開啓,運行,結束等狀態
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled)
{ /* DOES CALLOUT */
    CHECK_FOR_FORK();
    // 若是 runloop 正在回收中,直接返回 kCFRunLoopRunFinished ,表示 runloop 已經完成
    if (__CFRunLoopIsDeallocating(rl))
        return kCFRunLoopRunFinished;
    // 對 runloop 加鎖
    __CFRunLoopLock(rl);
    ✅// 從 runloop 中查找給定的 mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    ✅// 若是找不到 mode,且當前 runloop 的 currentMode 也爲空,進入 if 邏輯
    // __CFRunLoopModeIsEmpty 函數結果爲空的話,說明 runloop 已經處理完全部任務
    if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode))
    {
        Boolean did = false;
        // 若是 currentMode 不爲空
        if (currentMode)
            // 對 currentMode 解鎖
            __CFRunLoopModeUnlock(currentMode);
        // 對 runloop 解鎖
        __CFRunLoopUnlock(rl);
        return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
    }
    // 暫時取出 runloop 的 per_run_data
    volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
    ✅// 取出 runloop 的當前 mode
    CFRunLoopModeRef previousMode = rl->_currentMode;
    ✅// 將查找到的 mode 賦值到 runloop 的 _curentMode,也就是說在這 runloop 完成了 mode 的切換
    rl->_currentMode = currentMode;
    ✅// 初始化返回結果 result
    int32_t result = kCFRunLoopRunFinished;

    ✅// 若是註冊了 observer 監聽 kCFRunLoopEntry 狀態(即將進入 loop),則通知 observer
    if (currentMode->_observerMask & kCFRunLoopEntry)
        __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    ✅✅✅✅// runloop 核心函數 __CFRunLoopRun
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    ✅// 若是註冊了 observer 監聽 kCFRunLoopExit 狀態(即將推出 loop),則通知 observer
    if (currentMode->_observerMask & kCFRunLoopExit)
        __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    // 對 currentMode 解鎖
    __CFRunLoopModeUnlock(currentMode);
    // 還原原來的 previousPerRun
    __CFRunLoopPopPerRunData(rl, previousPerRun);
    // 還原原來的 mode
    rl->_currentMode = previousMode;
    // 對 runloop 解鎖
    __CFRunLoopUnlock(rl);
    return result;
}
複製代碼

CFRunLoopRun

CFRunLoopRun是RunLoop的核心函數,一次運行循環就是一次 CFRunLoopRun 的運行。其5個參數分別表明的意義以下:

  • CFRunLoopRef rl: CFRunLoopRef 對象
  • CFRunLoopModeRef rlm: mode 的名稱
  • CFTimeInterval seconds: 超時時間
  • Boolean stopAfterHandle: 處理完 source 後是否直接返回
  • CFRunLoopModeRef previousMode: 前一次運行循環的 mode
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode)
複製代碼

因爲CFRunLoopRun的函數過長,邏輯比較複雜,因此咱們精簡了代碼,只講解其中的一些的核心邏輯,主要有如下幾個步驟:

  1. 使用dispatch_source_t建立一個定時器來處理超時相關的邏輯,若是沒設置會默認一個特別大的數字
  2. 啓動do...while循環開始處理事件
  3. 通知 Observers: RunLoop 即將觸發 Timer 回調。
  4. 通知 Observers: RunLoop 即將觸發 Source0 (非port) 回調。
  5. 執行被加入的block
  6. 若是有 Source1 (基於port) 處於 ready 狀態,直接處理這個 Source1 而後跳轉去處理消息。
  7. 通知 Observers: RunLoop 的線程即將進入休眠(sleep)。
  8. 調用 mach_msg 等待接受 mach_port 的消息。線程將進入休眠, 直到被下面某一個事件喚醒。
    • 基於 port 的 Source 事件;
    • Timer 時間到;
    • RunLoop 自身的超時時間到了
    • 被其餘什麼調用者手動喚醒
  9. 通知 Observers: RunLoop 的線程剛剛被喚醒了。
  10. 收到消息,處理消息。
    • 若是一個 Timer 到時間了,觸發這個Timer的回調。
    • 若是有dispatch到main_queue的block,執行block。
    • 若是一個 Source1 (基於port) 發出事件了,處理這個事件
  11. 執行加入到Loop的block
  12. 根據當前 RunLoop 的狀態來判斷是否須要走下一個 loop。當被外部強制中止或 loop 超時時,就不繼續下一個 loop 了,不然繼續走下一個 loop 。
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode)
{
     // 聲明一個空的 GCD 定時器
    dispatch_source_t timeout_timer = NULL;
    // 初始化一個 「超時上下文」 結構體指針對象
    struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context));
    ...
    
    int32_t retVal = 0;
    do
    {
        // 通知 Observers 即將處理 Timers
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        
        // 通知 Observers 即將處理 Sources
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);

        // 處理 Blocks
        __CFRunLoopDoBlocks(rl, rlm);

        // 處理 Source0
        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;
        }

            
        didDispatchPortLastTime = false;

        // 通知 Observers 即將休眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
            
        __CFRunLoopSetSleeping(rl);

        CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent();
        
        do
        {
            if (kCFUseCollectableAllocator)
            {
                // objc_clear_stack(0);
                // <rdar://problem/16393959>
                memset(msg_buffer, 0, sizeof(msg_buffer));
            }
            msg = (mach_msg_header_t *)msg_buffer;

            // 等待別的消息來喚醒當前線程
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);

            if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort)
            {
                // Drain the internal queue. If one of the callout blocks sets the timerFired flag, break out and service the timer.
                while (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue))
                    ;
                if (rlm->_timerFired)
                {
                    // Leave livePort as the queue port, and service timers below
                    rlm->_timerFired = false;
                    break;
                }
                else
                {
                    if (msg && msg != (mach_msg_header_t *)msg_buffer)
                        free(msg);
                }
            }
            else
            {
                // Go ahead and leave the inner loop.
                break;
            }
        } while (1);

        // user callouts now OK again
        __CFRunLoopUnsetSleeping(rl);
        
        // 通知 Observers 結束休眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

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

複製代碼

RunLoop的退出

  1. 主線程銷燬RunLoop退出
  2. Mode中有一些Timer 、Source、 Observer,這些保證Mode不爲空時保證RunLoop沒有空轉而且是在運行的,當Mode中爲空的時候,RunLoop會馬上退出
  3. 咱們在啓動RunLoop的時候能夠設置何時中止
[NSRunLoop currentRunLoop]runUntilDate:<#(nonnull NSDate *)#>
[NSRunLoop currentRunLoop]runMode:<#(nonnull NSString *)#> beforeDate:<#(nonnull NSDate *)#>
複製代碼

RunLoop的應用

RunLoop在系統中的應用

AutoreleasePool

App啓動後,蘋果在主線程 RunLoop 裏註冊了兩個 Observer,其回調都是 _wrapRunLoopWithAutoreleasePoolHandler()

第一個 Observer 監視的事件是Entry(即將進入Loop),其回調內會調用 _objc_autoreleasePoolPush() 建立自動釋放池。其 order 是-2147483647,優先級最高,保證建立釋放池發生在其餘全部回調以前。

第二個 Observer 監視了兩個事件: BeforeWaiting(準備進入休眠) 時調用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 釋放舊的池並建立新池;Exit(即將退出Loop) 時調用 _objc_autoreleasePoolPop() 來釋放自動釋放池。這個 Observer 的 order 是 2147483647,優先級最低,保證其釋放池子發生在其餘全部回調以後。

在主線程執行的代碼,一般是寫在諸如事件回調、Timer回調內的。這些回調會被 RunLoop 建立好的 AutoreleasePool 環繞着,因此不會出現內存泄漏,開發者也沒必要顯示建立 Pool 了。

事件響應

蘋果註冊了一個 Source1 (基於 mach port 的) 用來接收系統事件,其回調函數爲 __IOHIDEventSystemClientQueueCallback()。

當一個硬件事件(觸摸/鎖屏/搖晃等)發生後,首先由 IOKit.framework 生成一個 IOHIDEvent 事件並由SpringBoard 接收。這個過程的詳細狀況能夠參考這裏。SpringBoard 只接收按鍵(鎖屏/靜音等),觸摸,加速,接近傳感器等幾種 Event,隨後用 mach port 轉發給須要的App進程。隨後蘋果註冊的那個 Source1 就會觸發回調,並調用 _UIApplicationHandleEventQueue() 進行應用內部的分發。

_UIApplicationHandleEventQueue() 會把IOHIDEvent 處理幷包裝成 UIEvent 進行處理或分發,其中包括識別 UIGesture/處理屏幕旋轉/發送給 UIWindow 等。一般事件好比 UIButton 點擊、touchesBegin/Move/End/Cancel 事件都是在這個回調中完成的。

手勢識別

當上面的 _UIApplicationHandleEventQueue() 識別了一個手勢時,其首先會調用 Cancel 將當前的 touchesBegin/Move/End 系列回調打斷。隨後系統將對應的 UIGestureRecognizer 標記爲待處理。

蘋果註冊了一個 Observer 監測 BeforeWaiting (Loop即將進入休眠) 事件,這個Observer的回調函數是 _UIGestureRecognizerUpdateObserver(),其內部會獲取全部剛被標記爲待處理的 GestureRecognizer,並執行GestureRecognizer的回調。

當有 UIGestureRecognizer 的變化(建立/銷燬/狀態改變)時,這個回調都會進行相應處理。

界面更新

當在操做 UI 時,好比改變了 Frame、更新了 UIView/CALayer 的層次時,或者手動調用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法後,這個 UIView/CALayer 就被標記爲待處理,並被提交到一個全局的容器去。

蘋果註冊了一個 Observer 監聽 BeforeWaiting(即將進入休眠) 和 Exit (即將退出Loop) 事件,回調去執行一個很長的函數: _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。這個函數裏會遍歷全部待處理的 UIView/CAlayer 以執行實際的繪製和調整,並更新 UI 界面。

這個函數內部的調用棧大概是這樣的:

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
    QuartzCore:CA::Transaction::observer_callback:
        CA::Transaction::commit();
            CA::Context::commit_transaction();
                CA::Layer::layout_and_display_if_needed();
                    CA::Layer::layout_if_needed();
                        [CALayer layoutSublayers];
                            [UIView layoutSubviews];
                    CA::Layer::display_if_needed();
                        [CALayer display];
                            [UIView drawRect];
複製代碼

定時器

NSTimer 其實就是 CFRunLoopTimerRef,他們之間是 toll-free bridged 的。一個 NSTimer 註冊到 RunLoop 後,RunLoop 會爲其重複的時間點註冊好事件。例如 10:00, 10:10, 10:20 這幾個時間點。RunLoop爲了節省資源,並不會在很是準確的時間點回調這個Timer。Timer 有個屬性叫作 Tolerance (寬容度),標示了當時間點到後,允許有多少最大偏差。

若是某個時間點被錯過了,例如執行了一個很長的任務,則那個時間點的回調也會跳過去,不會延後執行。就好比等公交,若是 10:10 時我忙着玩手機錯過了那個點的公交,那我只能等 10:20 這一趟了。

CADisplayLink 是一個和屏幕刷新率一致的定時器(但實際實現原理更復雜,和 NSTimer 並不同,其內部實際是操做了一個 Source)。若是在兩次屏幕刷新之間執行了一個長任務,那其中就會有一幀被跳過去(和 NSTimer 類似),形成界面卡頓的感受。在快速滑動TableView時,即便一幀的卡頓也會讓用戶有所察覺。Facebook 開源的 AsyncDisplayLink 就是爲了解決界面卡頓的問題,其內部也用到了 RunLoop.

PerformSelecter

當調用 NSObject 的 performSelecter:afterDelay: 後,實際上其內部會建立一個 Timer 並添加到當前線程的 RunLoop 中。因此若是當前線程沒有 RunLoop,則這個方法會失效。

當調用 performSelector:onThread: 時,實際上其會建立一個 Timer 加到對應的線程去,一樣的,若是對應線程沒有 RunLoop 該方法也會失效。

關於GCD

實際上 RunLoop 底層也會用到 GCD 的東西,NSTimer 是用了 XNU 內核的 mk_timer,我也仔細調試了一下,發現 NSTimer 確實是由 mk_timer 驅動,而非 GCD 驅動的)。但同時 GCD 提供的某些接口也用到了 RunLoop, 例如 dispatch_async()。

當調用 dispatch_async(dispatch_get_main_queue(), block) 時,libDispatch 會向主線程的 RunLoop 發送消息,RunLoop會被喚醒,並從消息中取得這個 block,並在回調 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() 裏執行這個 block。但這個邏輯僅限於 dispatch 到主線程,dispatch 到其餘線程仍然是由 libDispatch 處理的。

關於網絡請求

iOS 中,關於網絡請求的接口自下至上有以下幾層:

CFSocket
CFNetwork       ->ASIHttpRequest
NSURLConnection ->AFNetworking
NSURLSession    ->AFNetworking2, Alamofire
複製代碼
  • CFSocket 是最底層的接口,只負責 socket 通訊。
  • CFNetwork 是基於 CFSocket 等接口的上層封裝,ASIHttpRequest 工做於這一層。
  • NSURLConnection 是基於 CFNetwork 的更高層的封裝,提供面向對象的接口,AFNetworking 工做於這一層。
  • NSURLSession 是 iOS7 中新增的接口,表面上是和 NSURLConnection 並列的,但底層仍然用到了 NSURLConnection 的部分功能 (好比 com.apple.NSURLConnectionLoader 線程),AFNetworking2 和 Alamofire 工做於這一層。

下面主要介紹下 NSURLConnection 的工做過程。

一般使用 NSURLConnection 時,你會傳入一個 Delegate,當調用了 [connection start] 後,這個 Delegate 就會不停收到事件回調。實際上,start 這個函數的內部會會獲取 CurrentRunLoop,而後在其中的 DefaultMode 添加了4個 Source0 (即須要手動觸發的Source)。

CFMultiplexerSource 是負責各類 Delegate 回調的,

CFHTTPCookieStorage是處理各類 Cookie 的。

當開始網絡傳輸時,咱們能夠看到 NSURLConnection 建立了兩個新線程:com.apple.NSURLConnectionLoadercom.apple.CFSocket.private。其中 CFSocket 線程是處理底層 socket 鏈接的。NSURLConnectionLoader這個線程內部會使用 RunLoop 來接收底層 socket 的事件,並經過以前添加的 Source0通知到上層的 Delegate。

RunLoop在實際開發中的應用

AFNetworking

AFURLConnectionOperation 這個類是基於 NSURLConnection 構建的,其但願能在後臺線程接收 Delegate 回調。爲此 AFNetworking 單首創建了一個線程,並在這個線程中啓動了一個 RunLoop:

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}
 
+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });
    return _networkRequestThread;
}
複製代碼

RunLoop 啓動前內部必需要有至少一個Timer/Observer/Source,因此 AFNetworking 在 [runLoop run] 以前先建立了一個新的 NSMachPort 添加進去了。一般狀況下,調用者須要持有這個 NSMachPort (mach_port) 並在外部線程經過這個 port 發送消息到 loop 內;但此處添加 port 只是爲了讓 RunLoop 不至於退出,並無用於實際的發送消息。

- (void)start {
    [self.lock lock];
    if ([self isCancelled]) {
        [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    } else if ([self isReady]) {
        self.state = AFOperationExecutingState;
        [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    }
    [self.lock unlock];
}
複製代碼

當須要這個後臺線程執行任務時,AFNetworking 經過調用 [NSObject performSelector:onThread:..] 將這個任務扔到了後臺線程的 RunLoop 中。

TableView延遲加載圖片

將setImage放到NSDefaultRunLoopMode去作,也就是在滑動的時候並不會去調用這個方法,而是會等到滑動完畢切換到NSDefaultRunLoopMode下面纔會調用。

UIImage *downLoadImage = ...;  
[self.avatarImageView performSelector:@selector(setImage:)  
                        withObject:downloadImage  
                        afterDelay:0  
                        inModes:@[NSDefaultRunLoopMode]];
複製代碼

Crash的兼容處理

  1. program received signal:SIGABRT SIGABRT通常是過分release或者發送unrecogized selector致使。
  2. EXC_BAD_ACCESS是訪問已被釋放的內存致使,野指針錯誤。 由 SIGABRT 引發的Crash 是系統發這個signal給App,程序收到這個signal後,就會把主線程的RunLoop殺死,程序就Crash了 該例只針對 SIGABRT引發的Crash有效。

檢測卡頓

當App發生主線程卡頓時,咱們能夠經過RunLoop來監聽到相對應的堆棧信息,而後進行優化處理。

  • 要想監聽 RunLoop,你就首先須要建立一個 CFRunLoopObserverContext 觀察者
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopAllActivities,YES,0,&runLoopObserverCallBack,&context);
複製代碼
  • 將建立好的觀察者 runLoopObserver 添加到主線程 RunLoop 的 common 模式下觀察。而後,建立一個持續的子線程專門用來監控主線程的 RunLoop 狀態。
  • 一旦發現進入睡眠前的 kCFRunLoopBeforeSources 狀態,或者喚醒後的狀態 kCFRunLoopAfterWaiting,在設置的時間閾值內一直沒有變化,便可斷定爲卡頓。接下來,咱們就能夠 dump 出堆棧的信息,從而進一步分析出具體是哪一個方法的執行時間過長。
//建立子線程監控
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    //子線程開啓一個持續的 loop 用來進行監控
    while (YES) {
        long semaphoreWait = dispatch_semaphore_wait(dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC));
        if (semaphoreWait != 0) {
            if (!runLoopObserver) {
                timeoutCount = 0;
                dispatchSemaphore = 0;
                runLoopActivity = 0;
                return;
            }
            //BeforeSources 和 AfterWaiting 這兩個狀態可以檢測到是否卡頓
            if (runLoopActivity == kCFRunLoopBeforeSources || runLoopActivity == kCFRunLoopAfterWaiting) {
                //將堆棧信息上報服務器的代碼放到這裏
            } //end activity
        }// end semaphore wait
        timeoutCount = 0;
    }// end while
});
複製代碼

總結

  • RunLoop的做用和意義
    • 保持程序持續運行,程序一啓動就會開一個主線程,並接受用戶輸入
    • 處理App中的各類事件,並決定程序在什麼時候處理應該處理哪些Event
    • 節省CPU資源,提升程序性能,在須要休眠時休眠
    • 調用解耦,使用戶調用的時間有隊列來處理,調用方和處理方徹底解耦
  • Runloop與線程的關係
    • 每條線程都有惟一的一個與之對應的RunLoop對象
    • RunLoop保存在一個全局的Dictionary裏,線程做爲key,RunLoop做爲value
    • 主線程的RunLoop已經自動建立好了,子線程的RunLoop須要主動建立
    • RunLoop在第一次獲取時建立,在線程結束時銷燬
  • RunLoop的本質與結構
    • RunLoop本質也是一個結構體對象
    • RunloopMode是指的一個事件循環必須在某種模式下跑,系統會預約義幾個模式。一個Runloop有多個Mode
    • CFRunloopSource,CFRunloopTimer,CFRunloopObserver這些元素是在Mode裏面的,Mode與這些元素的對應關係也是1對多的。可是必須至少有一個Source或者Timer,由於若是Mode爲空,RunLoop運行到空模式不會進行空轉,就會馬上退出
    • CFRunloopSource分爲source0(處理用戶事件)和source1(處理內核事件)
    • CFRunloopObserver是監聽和通知Runloop狀態
  • RunLoop啓動邏輯
    • 從 runloop 中查找給定的 mode
    • 將查找到的 mode 賦值到 runloop 的 _curentMode,也就是說在這 runloop 完成了 mode 的切換
    • 核心函數 __CFRunLoopRun
    • 若是註冊了 observer,則通知runloop的開啓,運行,結束等狀態
  • CFRunLoopRun邏輯
    • 使用dispatch_source_t建立一個定時器來處理超時相關的邏輯,若是沒設置會默認一個特別大的數字
    • 啓動do...while循環開始處理事件
    • 通知 Observers: RunLoop 即將觸發 Timer和Source0 回調。
    • 執行被加入的block
    • 若是有 Source1 (基於port) 處於 ready 狀態,直接處理這個 Source1 而後跳轉去處理消息。
    • 通知 Observers: RunLoop 的線程即將進入休眠(sleep)。
    • 調用 mach_msg 等待接受 mach_port 的消息。線程將進入休眠, 直到被下面某一個事件喚醒。
    • 通知 Observers: RunLoop 的線程剛剛被喚醒了。
    • 收到消息,處理消息。(Timer,dispatch,Source1等),並執行block回調
    • 根據當前 RunLoop 的狀態來判斷是否須要走下一個 loop。當被外部強制中止或 loop 超時時,就不繼續下一個 loop 了,不然繼續走下一個 loop 。
  • RunLoop的退出
    • 主線程銷燬RunLoop退出
    • Mode中有一些Timer 、Source、 Observer,這些保證Mode不爲空時保證RunLoop沒有空轉而且是在運行的,當Mode中爲空的時候,RunLoop會馬上退出
    • 咱們在啓動RunLoop的時候能夠設置何時中止

參考

ibireme深刻理解RunLoop

孫源Runloop分享

戴明iOS高手課

iOS 查漏補缺 - RunLoop

相關文章
相關標籤/搜索