iOS底層學習 - 深入RunLoop

RunLoop這個名詞對于iOS開發(fā)來說應該是一個聽膩了的詞匯,而且只知其一不知其二,本篇章就來再深入復習一下RunLoop

RunLoop簡介

什么是RunLoop

一般來講,一個線程一次只能執(zhí)行一個任務,執(zhí)行完成后線程就會退出。如果我們需要一個機制,讓線程能隨時處理事件但并不退出,這種模型通常被稱作 Event Loop。 Event Loop 在很多系統(tǒng)和框架里都有實現(xiàn),比如 Node.js 的事件處理,比如 Windows 程序的消息循環(huán),再比如 OSX/iOS 里的 RunLoop

實現(xiàn)這種模型的關鍵點在于:管理事件/消息,讓線程在沒有處理消息時休眠以避免資源占用、在有消息到來時立刻被喚醒。

RunLoop作用

  1. 保持程序持續(xù)運行:程序一啟動就會開一個主線程,主線程一開起來就會跑一個主線程對應的RunLoop,RunLoop保證主線程不會被銷毀,也就保證了程序的持續(xù)運行
  2. 處理App中的各種事件(比如:觸摸事件,定時器事件,Selector事件等)
  3. 節(jié)省CPU資源,提高程序性能:程序運行起來時,當什么操作都沒有做的時候,RunLoop就告訴CPU,現(xiàn)在沒有事情做,我要去休息,這時CPU就會將其資源釋放出來去做其他的事情,當有事情做的時候RunLoop就會立馬起來去做事情

我們先通過API內(nèi)一張圖片來簡單看一下RunLoop內(nèi)部運行原理

image.png

為什么使用RunLoop

了解了RunLoop的作用,那么在蘋果系統(tǒng)中,為什么使用RunLoop呢?主要有一下幾點

  1. 使程序一直運行,并接受用戶輸入
  2. 決定程序在何時處理應該處理哪些Event
  3. 調(diào)用解耦(Message Queue): 比如一次滑屏事件,可能會觸發(fā)多條消息,所以必須有一個類似Message Queue的模塊去處理來解耦,形成一個隊列來依次處理,這樣用戶的調(diào)用方和處理方實現(xiàn)了完全解耦
  4. 節(jié)省CPU的時間和效率

RunLoop底層原理

我們還是查看源碼來進行探究

RunLoop代碼層級

  1. NSRunloop:最上層的NSRunloop層實際上是對C語言實現(xiàn)的CFRunloop的一個封裝,實際上它沒干什么事,比如CFRunloop有一個過期時間是double類型,NSRunloop把它變味了NSDate類型;
  2. CFRunloop:這是真正干事的一層,源代碼是開源的,并且是跨平臺的;
  3. 系統(tǒng)層:底層實現(xiàn)用到了GCD,mach kernel是蘋果的內(nèi)核,比如runloop的睡眠和喚醒就是用mach kernel來實現(xiàn)的。
image.png

RunLoop入口

在OC代碼中,Runloop是由系統(tǒng)默認開啟的,就再main函數(shù)中,會開啟主線程和RunLoop。如果沒有Runloop,那么main函數(shù)執(zhí)行完畢后,程序就退出了,這說明在UIApplicationMain函數(shù)中,開啟了一個和主線程相關的RunLoop,導致UIApplicationMain不會返回,一直在運行中,也就保證了程序的持續(xù)運行。

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

接著我們查看源碼,發(fā)現(xiàn)CFRunLoopRun的底層實現(xiàn)結構也非常簡單,就是一個do...while循環(huán),我們可以把RunLoop看成一個死循環(huán)。如果沒有RunLoop,UIApplicationMain函數(shù)執(zhí)行完畢之后將直接返回,也就沒有程序持續(xù)運行一說了。

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 開發(fā)中能遇到兩個線程對象: pthread_tNSThread。過去蘋果有份文檔標明了 NSThread只是 pthread_t 的封裝,但那份文檔已經(jīng)失效了,現(xiàn)在它們也有可能都是直接包裝自最底層的 mach thread。蘋果并沒有提供這兩個對象相互轉(zhuǎn)換的接口,但不管怎么樣,可以肯定的是 pthread_tNSThread 是一一對應的。比如,你可以通過 pthread_main_thread_np()[NSThread mainThread] 來獲取主線程;也可以通過 pthread_self()[NSThread currentThread] 來獲取當前線程。CFRunLoop 是基于 pthread 來管理的。

蘋果不允許直接創(chuàng)建 RunLoop,它只提供了兩個自動獲取的函數(shù):CFRunLoopGetMain()CFRunLoopGetCurrent()。 這兩個函數(shù)內(nèi)部的邏輯大概是下面這樣:

?// 獲得當前線程的RunLoop對象,內(nèi)部調(diào)用_CFRunLoopGet0函數(shù)
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,則創(chuàng)建
    if (!__CFRunLoops) {
        __CFUnlock(&loopsLock);
    CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
    ?// 根據(jù)傳入的主線程獲取主線程對應的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為空,則創(chuàng)建一個新的loop,所以runloop會在第一次獲取的時候創(chuàng)建
    if (!loop) {  
    CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
    loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));

    ?// 創(chuàng)建好之后,以線程為key runloop為value,一對一存儲在字典中,下次獲取的時候,則直接返回字典內(nèi)的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())) {
        ?// 注冊一個回調(diào),當線程銷毀時,順便也銷毀對應的RunLoop
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;
}
復制代碼

通過源碼分析可以看出,線程和RunLoop之間是一一對應的,其關系是保存在一個Dictionary字典里。所以我們創(chuàng)建子線程RunLoop時,只需在子線程中獲取當前線程的RunLoop對象即可[NSRunLoop currentRunLoop];。如果不獲取,那子線程就不會創(chuàng)建與之相關聯(lián)的RunLoop,并且只能在一個線程的內(nèi)部獲取其RunLoop。

當通過調(diào)用[NSRunLoop currentRunLoop];方法獲取RunLoop時,會先看一下字典里有沒有子線程對應的RunLoop,如果有則直接返回RunLoop,如果沒有則會創(chuàng)建一個,并將與之對應的子線程存入字典中。當線程結束時,RunLoop會被銷毀。

總結一下Runloop與線程的關系

  1. 每條線程都有唯一的一個與之對應的RunLoop對象
  2. RunLoop保存在一個全局的Dictionary里,線程作為key,RunLoop作為value
  3. 主線程的RunLoop已經(jīng)自動創(chuàng)建好了,子線程的RunLoop需要主動創(chuàng)建
  4. RunLoop在第一次獲取時創(chuàng)建,在線程結束時銷毀

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 運行會重置的一個數(shù)據(jù)結構
    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 內(nèi)部 block 鏈表表頭指針
    struct _block_item *_blocks_head;
    // runloop 內(nèi)部 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;

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

};
復制代碼

通過查看RunLoop的底層結構,我們發(fā)現(xiàn)了RunLoop也是一個結構體對象,其中有幾個主要的變量:

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

通過上述變量,我們可以知道:

  • RunLoop可以有多個mode對象
  • Runloop在同一時間只能且必須在某一種特定的Mode下面Run,更換Mode時,必須要停止當前的Loop,然后重啟新的Loop,重啟的意思是退出當前的while循環(huán),然后重新設置一個新的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 定時器觸發(fā)時設置為 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 */
};

復制代碼

我們發(fā)現(xiàn),一個CFRunLoopModeRef也包含很多變量,主要有_sources0,_sources0兩個集合和_observers,_timers兩個數(shù)組。

這說明一個mode可以包含多種items模式

CFRunLoopSourceRef

CFRunLoopSourceRef是事件源(輸入源)。通過源碼可以發(fā)現(xiàn),其分為source0source1兩個。

  • source0:處理App內(nèi)部事件,App自己負責管理(觸發(fā)),如UIEvent,CFSocket等;
  • source1:由Runloop和內(nèi)核管理,mach port驅(qū)動,如CFMachPort(輕量級的進程間通信的方式,NSPort就是對它的封裝,還有Runloop的睡眠和喚醒就是通過它來做的),CFMessagePort
typedef struct __CFRunLoopSource * CFRunLoopSourceRef;

struct __CFRunLoopSource
{
    // CoreFoundation 中的 runtime 基礎信息
    CFRuntimeBase _base;
    uint32_t _bits;
    // 互斥鎖
    pthread_mutex_t _lock;
    // source 的優(yōu)先級,值為小,優(yōu)先級越高
    CFIndex _order; /* immutable */
    // runloop 集合
    CFMutableBagRef _runLoops;
    // 一個聯(lián)合體,說明 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 相等的函數(shù)
    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 要執(zhí)行的任務塊
    void    (*perform)(void *info);
} CFRunLoopSourceContext;

復制代碼

CFRunLoopObserverRef

CFRunLoopObserverRef是觀察者,每個Observer都包含了一個回調(diào)(函數(shù)指針),當RunLoop的狀態(tài)發(fā)生變化時,觀察者就能通過回調(diào)接受到這個變化。主要是用來向外界報告Runloop當前的狀態(tài)的更改。

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 優(yōu)先級
    CFIndex _order;                     /* immutable */
    // observer 回調(diào)函數(shù)
    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 是基于時間的觸發(fā)器,它和NSTimer是toll-free bridged的,可以混用。其包含一個時間長度和一個回調(diào)(函數(shù)指針)。

當其加入到RunLoop時,RunLoop會注冊對應的時間點,當時間點到時,RunLoop會被喚醒以執(zhí)行那個回調(diào)。

總結一下關于RunLoop的結構

  1. RunLoop本質(zhì)也是一個結構體對象
  2. RunloopMode是指的一個事件循環(huán)必須在某種模式下跑,系統(tǒng)會預定義幾個模式。一個Runloop有多個Mode;
  3. CFRunloopSourceCFRunloopTimer,CFRunloopObserver這些元素是在Mode里面的,Mode與這些元素的對應關系也是1對多的。但是必須至少有一個Source或者Timer,因為如果Mode為空,RunLoop運行到空模式不會進行空轉(zhuǎn),就會立刻退出。
  4. CFRunloopSource分為source0(處理用戶事件)和source1(處理內(nèi)核事件)
  5. CFRunloopObserver是監(jiān)聽和通知Runloop狀態(tài)
image.png

RunLoop的Mode

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

1\. kCFRunLoopDefaultMode:App的默認Mode,通常主線程是在這個Mode下運行
2\. UITrackingRunLoopMode:界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響
3\. UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode,啟動完成后就不再使用,會切換到kCFRunLoopDefaultMode
4\. GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內(nèi)部 Mode,通常用不到
5\. kCFRunLoopCommonModes: 這是一個占位用的Mode,作為標記kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一種真正的Mode 
復制代碼

Mode間的切換

我們平時在開發(fā)中一定遇到過,當我們使用NSTimer每一段時間執(zhí)行一些事情時滑動UIScrollView,NSTimer就會暫停,當我們停止滑動以后,NSTimer又會重新恢復的情況,這是由于RunloopMode必須在同一個模式下跑。

主線程的 RunLoop 里有兩個預置的 Mode:kCFRunLoopDefaultModeUITrackingRunLoopMode。這兩個 Mode 都已經(jīng)被標記為”Common”屬性。DefaultMode 是 App 平時所處的狀態(tài),TrackingRunLoopMode是追蹤 ScrollView 滑動時的狀態(tài)。當你創(chuàng)建一個 Timer 并加到 DefaultMode 時,Timer 會得到重復回調(diào),但此時滑動一個TableView時,RunLoop 會將 mode 切換為 TrackingRunLoopMode,這時 Timer 就不會被回調(diào),并且也不會影響到滑動操作。

有時你需要一個 Timer,在兩個 Mode 中都能得到回調(diào),一種辦法就是將這個 Timer 分別加入這兩個 Mode。還有一種方式,就是將 Timer 加入到頂層的 RunLoop 的 “commonModeItems”?!眂ommonModeItems” 被 RunLoop 自動更新到所有具有”Common”屬性的 Mode 里去。

一個 Mode 可以將自己標記為”Common”屬性(通過將其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每當 RunLoop 的內(nèi)容發(fā)生變化時,RunLoop 都會自動將 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 標記的所有Mode里。

RunLoop啟動邏輯

我們知道在main函數(shù)啟動時,會有Runloop的用DefaultMode默認啟動和使用指定Mode進行啟動,相關的源碼如下,可以發(fā)現(xiàn),其核心邏輯都是調(diào)用了CFRunLoopRunSpecific函數(shù)

/// 用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函數(shù),根據(jù)其代碼,主要總結為以下幾個步驟:

  • 從 runloop 中查找給定的 mode
  • 將查找到的 mode 賦值到 runloop 的 _curentMode,也就是說在這 runloop 完成了 mode 的切換
  • 調(diào)用核心函數(shù) __CFRunLoopRun
  • 如果注冊了 observer,則通知runloop的開啟,運行,結束等狀態(tài)
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled)
{ /* DOES CALLOUT */
    CHECK_FOR_FORK();
    // 如果 runloop 正在回收中,直接返回 kCFRunLoopRunFinished ,表示 runloop 已經(jīng)完成
    if (__CFRunLoopIsDeallocating(rl))
        return kCFRunLoopRunFinished;
    // 對 runloop 加鎖
    __CFRunLoopLock(rl);
    ?// 從 runloop 中查找給定的 mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    ?// 如果找不到 mode,且當前 runloop 的 currentMode 也為空,進入 if 邏輯
    // __CFRunLoopModeIsEmpty 函數(shù)結果為空的話,說明 runloop 已經(jīng)處理完所有任務
    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 監(jiān)聽 kCFRunLoopEntry 狀態(tài)(即將進入 loop),則通知 observer
    if (currentMode->_observerMask & kCFRunLoopEntry)
        __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    ????// runloop 核心函數(shù) __CFRunLoopRun
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    ?// 如果注冊了 observer 監(jiān)聽 kCFRunLoopExit 狀態(tài)(即將推出 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的核心函數(shù),一次運行循環(huán)就是一次 CFRunLoopRun 的運行。其5個參數(shù)分別代表的意義如下:

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

由于CFRunLoopRun的函數(shù)過長,邏輯比較復雜,所以我們精簡了代碼,只講解其中的一些的核心邏輯,主要有以下幾個步驟:

  1. 使用dispatch_source_t創(chuàng)建一個定時器來處理超時相關的邏輯,如果沒設置會默認一個特別大的數(shù)字
  2. 啟動do...while循環(huán)開始處理事件
  3. 通知 Observers: RunLoop 即將觸發(fā) Timer 回調(diào)。
  4. 通知 Observers: RunLoop 即將觸發(fā) Source0 (非port) 回調(diào)。
  5. 執(zhí)行被加入的block
  6. 如果有 Source1 (基于port) 處于 ready 狀態(tài),直接處理這個 Source1 然后跳轉(zhuǎn)去處理消息。
  7. 通知 Observers: RunLoop 的線程即將進入休眠(sleep)。
  8. 調(diào)用 mach_msg 等待接受 mach_port 的消息。線程將進入休眠, 直到被下面某一個事件喚醒。
    • 基于 port 的 Source 事件;
    • Timer 時間到;
    • RunLoop 自身的超時時間到了
    • 被其他什么調(diào)用者手動喚醒
  9. 通知 Observers: RunLoop 的線程剛剛被喚醒了。
  10. 收到消息,處理消息。
*   如果一個 Timer 到時間了,觸發(fā)這個Timer的回調(diào)。
*   如果有dispatch到main_queue的block,執(zhí)行block。
*   如果一個 Source1 (基于port) 發(fā)出事件了,處理這個事件
  1. 執(zhí)行加入到Loop的block
  2. 根據(jù)當前 RunLoop 的狀態(tài)來判斷是否需要走下一個 loop。當被外部強制停止或 loop 超時時,就不繼續(xù)下一個 loop 了,否則繼續(xù)走下一個 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,就跳轉(zhuǎn)到 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;
}

復制代碼
image.png

RunLoop的退出

  1. 主線程銷毀RunLoop退出
  2. Mode中有一些Timer 、Source、 Observer,這些保證Mode不為空時保證RunLoop沒有空轉(zhuǎn)并且是在運行的,當Mode中為空的時候,RunLoop會立刻退出
  3. 我們在啟動RunLoop的時候可以設置什么時候停止
[NSRunLoop currentRunLoop]runUntilDate:<#(nonnull NSDate *)#>
[NSRunLoop currentRunLoop]runMode:<#(nonnull NSString *)#> beforeDate:<#(nonnull NSDate *)#>
復制代碼

RunLoop的應用

RunLoop在系統(tǒng)中的應用

AutoreleasePool

App啟動后,蘋果在主線程 RunLoop 里注冊了兩個 Observer,其回調(diào)都是 _wrapRunLoopWithAutoreleasePoolHandler()。

第一個 Observer 監(jiān)視的事件是Entry(即將進入Loop),其回調(diào)內(nèi)會調(diào)用 _objc_autoreleasePoolPush() 創(chuàng)建自動釋放池。其 order 是-2147483647,優(yōu)先級最高,保證創(chuàng)建釋放池發(fā)生在其他所有回調(diào)之前。

第二個 Observer 監(jiān)視了兩個事件: BeforeWaiting(準備進入休眠) 時調(diào)用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 釋放舊的池并創(chuàng)建新池;Exit(即將退出Loop) 時調(diào)用 _objc_autoreleasePoolPop() 來釋放自動釋放池。這個 Observer 的 order 是 2147483647,優(yōu)先級最低,保證其釋放池子發(fā)生在其他所有回調(diào)之后。

在主線程執(zhí)行的代碼,通常是寫在諸如事件回調(diào)、Timer回調(diào)內(nèi)的。這些回調(diào)會被 RunLoop 創(chuàng)建好的 AutoreleasePool 環(huán)繞著,所以不會出現(xiàn)內(nèi)存泄漏,開發(fā)者也不必顯示創(chuàng)建 Pool 了。

事件響應

蘋果注冊了一個 Source1 (基于 mach port 的) 用來接收系統(tǒng)事件,其回調(diào)函數(shù)為 __IOHIDEventSystemClientQueueCallback()。

當一個硬件事件(觸摸/鎖屏/搖晃等)發(fā)生后,首先由 IOKit.framework 生成一個 IOHIDEvent 事件并由SpringBoard 接收。這個過程的詳細情況可以參考這里。SpringBoard 只接收按鍵(鎖屏/靜音等),觸摸,加速,接近傳感器等幾種 Event,隨后用 mach port 轉(zhuǎn)發(fā)給需要的App進程。隨后蘋果注冊的那個 Source1 就會觸發(fā)回調(diào),并調(diào)用 _UIApplicationHandleEventQueue() 進行應用內(nèi)部的分發(fā)。

_UIApplicationHandleEventQueue() 會把IOHIDEvent 處理并包裝成 UIEvent 進行處理或分發(fā),其中包括識別 UIGesture/處理屏幕旋轉(zhuǎn)/發(fā)送給 UIWindow 等。通常事件比如 UIButton 點擊、touchesBegin/Move/End/Cancel 事件都是在這個回調(diào)中完成的。

手勢識別

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

蘋果注冊了一個 Observer 監(jiān)測 BeforeWaiting (Loop即將進入休眠) 事件,這個Observer的回調(diào)函數(shù)是 _UIGestureRecognizerUpdateObserver(),其內(nèi)部會獲取所有剛被標記為待處理的 GestureRecognizer,并執(zhí)行GestureRecognizer的回調(diào)。

當有 UIGestureRecognizer 的變化(創(chuàng)建/銷毀/狀態(tài)改變)時,這個回調(diào)都會進行相應處理。

界面更新

當在操作 UI 時,比如改變了 Frame、更新了 UIView/CALayer 的層次時,或者手動調(diào)用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,這個 UIView/CALayer 就被標記為待處理,并被提交到一個全局的容器去。

蘋果注冊了一個 Observer 監(jiān)聽 BeforeWaiting(即將進入休眠) 和 Exit (即將退出Loop) 事件,回調(diào)去執(zhí)行一個很長的函數(shù): _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。這個函數(shù)里會遍歷所有待處理的 UIView/CAlayer 以執(zhí)行實際的繪制和調(diào)整,并更新 UI 界面。

這個函數(shù)內(nèi)部的調(diào)用棧大概是這樣的:

_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為了節(jié)省資源,并不會在非常準確的時間點回調(diào)這個Timer。Timer 有個屬性叫做 Tolerance (寬容度),標示了當時間點到后,容許有多少最大誤差。

如果某個時間點被錯過了,例如執(zhí)行了一個很長的任務,則那個時間點的回調(diào)也會跳過去,不會延后執(zhí)行。就比如等公交,如果 10:10 時我忙著玩手機錯過了那個點的公交,那我只能等 10:20 這一趟了。

CADisplayLink 是一個和屏幕刷新率一致的定時器(但實際實現(xiàn)原理更復雜,和 NSTimer 并不一樣,其內(nèi)部實際是操作了一個 Source)。如果在兩次屏幕刷新之間執(zhí)行了一個長任務,那其中就會有一幀被跳過去(和 NSTimer 相似),造成界面卡頓的感覺。在快速滑動TableView時,即使一幀的卡頓也會讓用戶有所察覺。Facebook 開源的 AsyncDisplayLink 就是為了解決界面卡頓的問題,其內(nèi)部也用到了 RunLoop.

PerformSelecter

當調(diào)用 NSObject 的 performSelecter:afterDelay: 后,實際上其內(nèi)部會創(chuàng)建一個 Timer 并添加到當前線程的 RunLoop 中。所以如果當前線程沒有 RunLoop,則這個方法會失效。

當調(diào)用 performSelector:onThread: 時,實際上其會創(chuàng)建一個 Timer 加到對應的線程去,同樣的,如果對應線程沒有 RunLoop 該方法也會失效。

關于GCD

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

當調(diào)用 dispatch_async(dispatch_get_main_queue(), block) 時,libDispatch 會向主線程的 RunLoop 發(fā)送消息,RunLoop會被喚醒,并從消息中取得這個 block,并在回調(diào) __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() 里執(zhí)行這個 block。但這個邏輯僅限于 dispatch 到主線程,dispatch 到其他線程仍然是由 libDispatch 處理的。

關于網(wǎng)絡請求

iOS 中,關于網(wǎng)絡請求的接口自下至上有如下幾層:

CFSocket
CFNetwork       ->ASIHttpRequest
NSURLConnection ->AFNetworking
NSURLSession    ->AFNetworking2, Alamofire
復制代碼
  • CFSocket 是最底層的接口,只負責 socket 通信。
  • CFNetwork 是基于 CFSocket 等接口的上層封裝,ASIHttpRequest 工作于這一層。
  • NSURLConnection 是基于 CFNetwork 的更高層的封裝,提供面向?qū)ο蟮慕涌?,AFNetworking 工作于這一層。
  • NSURLSession 是 iOS7 中新增的接口,表面上是和 NSURLConnection 并列的,但底層仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 線程),AFNetworking2 和 Alamofire 工作于這一層。

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

通常使用 NSURLConnection 時,你會傳入一個 Delegate,當調(diào)用了 [connection start] 后,這個 Delegate 就會不停收到事件回調(diào)。實際上,start 這個函數(shù)的內(nèi)部會會獲取 CurrentRunLoop,然后在其中的 DefaultMode 添加了4個 Source0 (即需要手動觸發(fā)的Source)。

CFMultiplexerSource 是負責各種 Delegate 回調(diào)的,

CFHTTPCookieStorage是處理各種 Cookie 的。

當開始網(wǎng)絡傳輸時,我們可以看到 NSURLConnection 創(chuàng)建了兩個新線程:com.apple.NSURLConnectionLoadercom.apple.CFSocket.private。其中 CFSocket 線程是處理底層 socket 連接的。NSURLConnectionLoader這個線程內(nèi)部會使用 RunLoop 來接收底層 socket 的事件,并通過之前添加的 Source0通知到上層的 Delegate。

image.png

RunLoop在實際開發(fā)中的應用

AFNetworking

AFURLConnectionOperation 這個類是基于 NSURLConnection 構建的,其希望能在后臺線程接收 Delegate 回調(diào)。為此 AFNetworking 單獨創(chuàng)建了一個線程,并在這個線程中啟動了一個 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 啟動前內(nèi)部必須要有至少一個Timer/Observer/Source,所以 AFNetworking 在 [runLoop run] 之前先創(chuàng)建了一個新的 NSMachPort 添加進去了。通常情況下,調(diào)用者需要持有這個 NSMachPort (mach_port) 并在外部線程通過這個 port 發(fā)送消息到 loop 內(nèi);但此處添加 port 只是為了讓 RunLoop 不至于退出,并沒有用于實際的發(fā)送消息。

- (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];
}
復制代碼

當需要這個后臺線程執(zhí)行任務時,AFNetworking 通過調(diào)用 [NSObject performSelector:onThread:..] 將這個任務扔到了后臺線程的 RunLoop 中。

TableView延遲加載圖片

將setImage放到NSDefaultRunLoopMode去做,也就是在滑動的時候并不會去調(diào)用這個方法,而是會等到滑動完畢切換到NSDefaultRunLoopMode下面才會調(diào)用。

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

Crash的兼容處理

  1. program received signal:SIGABRT SIGABRT一般是過度release或者發(fā)送unrecogized selector導致。
  2. EXC_BAD_ACCESS是訪問已被釋放的內(nèi)存導致,野指針錯誤。 由 SIGABRT 引起的Crash 是系統(tǒng)發(fā)這個signal給App,程序收到這個signal后,就會把主線程的RunLoop殺死,程序就Crash了 該例只針對 SIGABRT引起的Crash有效。

檢測卡頓

當App發(fā)生主線程卡頓時,我們可以通過RunLoop來監(jiān)聽到相對應的堆棧信息,然后進行優(yōu)化處理。

  • 要想監(jiān)聽 RunLoop,你就首先需要創(chuàng)建一個 CFRunLoopObserverContext 觀察者
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopAllActivities,YES,0,&runLoopObserverCallBack,&context);
復制代碼
  • 將創(chuàng)建好的觀察者 runLoopObserver 添加到主線程 RunLoop 的 common 模式下觀察。然后,創(chuàng)建一個持續(xù)的子線程專門用來監(jiān)控主線程的 RunLoop 狀態(tài)。
  • 一旦發(fā)現(xiàn)進入睡眠前的 kCFRunLoopBeforeSources 狀態(tài),或者喚醒后的狀態(tài) kCFRunLoopAfterWaiting,在設置的時間閾值內(nèi)一直沒有變化,即可判定為卡頓。接下來,我們就可以 dump 出堆棧的信息,從而進一步分析出具體是哪個方法的執(zhí)行時間過長。

//創(chuàng)建子線程監(jiān)控
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    //子線程開啟一個持續(xù)的 loop 用來進行監(jiān)控
    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 這兩個狀態(tài)能夠檢測到是否卡頓
            if (runLoopActivity == kCFRunLoopBeforeSources || runLoopActivity == kCFRunLoopAfterWaiting) {
                //將堆棧信息上報服務器的代碼放到這里
            } //end activity
        }// end semaphore wait
        timeoutCount = 0;
    }// end while
});
復制代碼

總結

  • RunLoop的作用和意義
    • 保持程序持續(xù)運行,程序一啟動就會開一個主線程,并接受用戶輸入
    • 處理App中的各種事件,并決定程序在何時處理應該處理哪些Event
    • 節(jié)省CPU資源,提高程序性能,在需要休眠時休眠
    • 調(diào)用解耦,使用戶調(diào)用的時間有隊列來處理,調(diào)用方和處理方完全解耦
  • Runloop與線程的關系
    • 每條線程都有唯一的一個與之對應的RunLoop對象
    • RunLoop保存在一個全局的Dictionary里,線程作為key,RunLoop作為value
    • 主線程的RunLoop已經(jīng)自動創(chuàng)建好了,子線程的RunLoop需要主動創(chuàng)建
    • RunLoop在第一次獲取時創(chuàng)建,在線程結束時銷毀
  • RunLoop的本質(zhì)與結構
    • RunLoop本質(zhì)也是一個結構體對象
    • RunloopMode是指的一個事件循環(huán)必須在某種模式下跑,系統(tǒng)會預定義幾個模式。一個Runloop有多個Mode
    • CFRunloopSource,CFRunloopTimer,CFRunloopObserver這些元素是在Mode里面的,Mode與這些元素的對應關系也是1對多的。但是必須至少有一個Source或者Timer,因為如果Mode為空,RunLoop運行到空模式不會進行空轉(zhuǎn),就會立刻退出
    • CFRunloopSource分為source0(處理用戶事件)和source1(處理內(nèi)核事件)
    • CFRunloopObserver是監(jiān)聽和通知Runloop狀態(tài)
  • RunLoop啟動邏輯
    • 從 runloop 中查找給定的 mode
    • 將查找到的 mode 賦值到 runloop 的 _curentMode,也就是說在這 runloop 完成了 mode 的切換
    • 核心函數(shù) __CFRunLoopRun
    • 如果注冊了 observer,則通知runloop的開啟,運行,結束等狀態(tài)
  • CFRunLoopRun邏輯
    • 使用dispatch_source_t創(chuàng)建一個定時器來處理超時相關的邏輯,如果沒設置會默認一個特別大的數(shù)字
    • 啟動do...while循環(huán)開始處理事件
    • 通知 Observers: RunLoop 即將觸發(fā) Timer和Source0 回調(diào)。
    • 執(zhí)行被加入的block
    • 如果有 Source1 (基于port) 處于 ready 狀態(tài),直接處理這個 Source1 然后跳轉(zhuǎn)去處理消息。
    • 通知 Observers: RunLoop 的線程即將進入休眠(sleep)。
    • 調(diào)用 mach_msg 等待接受 mach_port 的消息。線程將進入休眠, 直到被下面某一個事件喚醒。
    • 通知 Observers: RunLoop 的線程剛剛被喚醒了。
    • 收到消息,處理消息。(Timer,dispatch,Source1等),并執(zhí)行block回調(diào)
    • 根據(jù)當前 RunLoop 的狀態(tài)來判斷是否需要走下一個 loop。當被外部強制停止或 loop 超時時,就不繼續(xù)下一個 loop 了,否則繼續(xù)走下一個 loop 。
  • RunLoop的退出
    • 主線程銷毀RunLoop退出
    • Mode中有一些Timer 、Source、 Observer,這些保證Mode不為空時保證RunLoop沒有空轉(zhuǎn)并且是在運行的,當Mode中為空的時候,RunLoop會立刻退出
    • 我們在啟動RunLoop的時候可以設置什么時候停止

以下文章可以做一個學習參考:
GCD面試要點
block面試要點
Runtime面試要點
RunLoop面試要點
內(nèi)存管理面試要點
MVC、MVVM面試要點
網(wǎng)絡性能優(yōu)化面試要點
網(wǎng)絡編程面試要點
KVC&KVO面試要點
數(shù)據(jù)存儲面試要點
混編技術面試要點
設計模式面試要點
UI面試要點

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容