iOS RunLoop詳解

一、RunLoop初探

1.一、RunLoop是什麼?

RunLoop從字面上來講是跑圈的意思,若是這樣理解難免有些膚淺。下面是蘋果官方文檔的關於RunLoop的一段說明。html

Run loops are part of the fundamental infrastructure associated with threads. A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.segmentfault

這段話翻譯成中文以下:windows

RunLoop是與線程息息相關的基本基礎結構的一部分。RunLoop是一個調度任務和處理任務的事件循環。RunLoop的目的是爲了在有工做的時讓線程忙起來,而在沒工做時讓線程進入睡眠狀態。數組

簡單的說RunLoop是一種高級的循環機制,讓程序持續運行,並處理程序中的各類事件,讓線程在須要作事的時候忙起來,不須要的話就讓線程休眠。緩存

1.二、RunLoop與線程

從上面關於RunLoop的定義咱們能夠知道,RunLoop和線程有着密不可分的關係。一般狀況下線程的做用是用來執行一個或多個特定的任務,在線程執行完成以後就會退出再也不執行任務,RunLoop這樣的循環機制會讓線程可以不斷地執行任務並不退出。bash

Run loop management is not entirely automatic. You must still design your thread’s code to start the run loop at appropriate times and respond to incoming events. Both Cocoa and Core Foundation provide run loop objects to help you configure and manage your thread’s run loop. Your application does not need to create these objects explicitly; each thread, including the application’s main thread, has an associated run loop object. Only secondary threads need to run their run loop explicitly, however. The app frameworks automatically set up and run the run loop on the main thread as part of the application startup process.
【譯】RunLoop管理並非徹底自動的。須要設計一個線程在合適的時機啓動並響應傳入的事件,您仍然必須設計線程的代碼以在適當的時候啓動運行循環並響應傳入的事件。 CocoaCore Foundation都提供RunLoop對象,以幫助配置和管理線程的RunLoop。應用程序並不須要顯式建立這些對象。每一個線程(包括應用程序的主線程)都有一個關聯的RunLoop對象。可是,在子線程中須要顯式地運行RunLoop。在應用程序啓動過程當中,應用程序框架會自動在主線程上設置並運行RunLoop。app

從上面這一點話中咱們獲取到以下幾點信息:框架

  • RunLoop和線程是綁定在一塊兒的,每條線程都有惟一一個與之對應的RunLoop對象
  • 不能本身建立RunLoop對象,可是能夠獲取系統提供的RunLoop對象
  • 主線程的RunLoop對象是由系統自動建立好的,在應用程序啓動的時候會自動完成啓動,而子線程中的RunLoop對象須要咱們手動獲取並啓動。

RunLoop與線程的關係以下圖所示:less

從上圖中能夠看出,RunLoop在線程中不斷檢測,經過input sourcetimer source接受事件,而後通知線程進行處理事件。async

1.三、RunLoop的結構

以下是RunLoop的結構定義源碼:

struct __CFRunLoop {
    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;
    //commonMode的item集合
    CFMutableSetRef _commonModeItems;
    //當前的mode
    CFRunLoopModeRef _currentMode;
    //存儲的是CFRunLoopModeRef
    CFMutableSetRef _modes;
     // _block_item鏈表表頭指針
    struct _block_item *_blocks_head;
    // _block_item鏈表表尾指針
    struct _block_item *_blocks_tail;
    //運行時間點
    CFAbsoluteTime _runTime;
    //睡眠時間點
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
};
複製代碼

從上面RunLoop的源碼不難看出,一個RunLoop對象包含一個線程(_pthread),若干個mode(_modes),若干個commonMode(_commonModes)。 無論是mode仍是commonMode其類型都是CFRunLoopMode,只是在處理上有所不一樣。

1.四、CFRunLoopModeRef

以下所示是CFRunLoopMode的源碼。

struct __CFRunLoopMode {
    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];
    //sources0事件
    CFMutableSetRef _sources0;
    //sources1事件
    CFMutableSetRef _sources1;
    //observers事件
    CFMutableArrayRef _observers;
    //timers事件
    CFMutableArrayRef _timers;
    //字典  key是mach_port_t,value是CFRunLoopSourceRef
    CFMutableDictionaryRef _portToV1SourceMap;
    //保存全部須要監聽的port,好比_wakeUpPort,_timerPort都保存在這個數組中
    __CFPortSet _portSet;
    CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    //GCD定時器
    dispatch_source_t _timerSource;
    //GCD隊列
    dispatch_queue_t _queue;
    // 當定時器觸發時設置爲true
    Boolean _timerFired; // set to true by the source when a timer has fired
    Boolean _dispatchTimerArmed;
#endif
#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 */
};
複製代碼

CFRunLoopMode的源碼不難看出,一個CFRunLoopMode對象有惟一一個name,若干個sources0事件,若干個sources1事件,若干個timer事件,若干個observer事件和若干portRunLoop老是在某種特定的CFRunLoopMode下運行的,這個特定的mode即是_currentMode。而CFRunloopRef對應結構體的定義知道一個RunLoop對象包含有若干個mode,那麼就造成了以下如所示的結構。

關於CFRunLoopMode,蘋果提到了5個Model,分別是NSDefaultRunLoopModeNSConnectionReplyModeNSModalPanelRunLoopModeNSEventTrackingRunLoopModeNSRunLoopCommonModes。在iOS中公開暴露只有NSDefaultRunLoopModeNSRunLoopCommonModes

  • NSDefaultRunLoopMode:默認模式是用於大多數操做的模式。大多數時候使用此模式來啓動RunLoop並配置輸入源。
  • NSConnectionReplyMode:Cocoa將此模式與NSConnection對象結合使用以監測迴應。幾乎不須要本身使用此模式。
  • NSModalPanelRunLoopMode:Cocoa使用此模式來識別用於模式面板的事件。
  • NSEventTrackingRunLoopMode:Cocoa使用此模式來限制鼠標拖動loop和其餘類型的用戶界面跟蹤loop期間的傳入事件。一般用不到。
  • NSRunLoopCommonModes:是NSDefaultRunLoopModeNSEventTrackingRunLoopMode集合,在這種模式下RunLoop分別註冊了NSDefaultRunLoopModeUITrackingRunLoopMode。固然也能夠經過調用CFRunLoopAddCommonMode()方法將自定義Mode放到kCFRunLoopCommonModes組合。

1.五、CFRunLoopSourceRef-事件源

以下是CFRunLoopSource的源碼。

struct __CFRunLoopSource {
    CFRuntimeBase _base;
    //用於標記Signaled狀態,source0只有在被標記爲Signaled狀態,纔會被處理
    uint32_t _bits;
    pthread_mutex_t _lock;
    CFIndex _order;            /* immutable */
    CFMutableBagRef _runLoops;
    //聯合體 
    union {
        CFRunLoopSourceContext version0;    /* immutable, except invalidation */
        CFRunLoopSourceContext1 version1;    /* immutable, except invalidation */
    } _context;
};
複製代碼

根據蘋果官方的定義CFRunLoopSource是輸入源的抽象,分爲source0和source1兩個版本。

  • source0:是App內部事件,只包含一個函數指針回調,並不能主動觸發事件,使用時,你須要先調用CFRunLoopSourceSignal(source),將這個source標記爲待處理,而後手動調用CFRunLoopWakeUp(runloop)來喚醒RunLoop,讓其處理這個事件。
  • source1source1包含一個mach_port和一個函數回調指針。source1是基於port的,經過讀取某個port上內核消息隊列上的消息來決定執行的任務,而後再分發到sources0中處理的。source1只供系統使用,並不對開發者開放。

1.六、CFRunLoopTimerRef--Timer事件

CFRunLoopTimer是定時器。下面是CFRunLoopTimer的源碼:

struct __CFRunLoopTimer {
    CFRuntimeBase _base;
    uint16_t _bits;
    pthread_mutex_t _lock;
    //timer對應的runloop
    CFRunLoopRef _runLoop;
    //timer對應的mode
    CFMutableSetRef _rlModes;
    //下一次觸發的時間
    CFAbsoluteTime _nextFireDate;
    //定時的間隔
    CFTimeInterval _interval;        /* immutable */
    //定時器容許的偏差
    CFTimeInterval _tolerance;          /* mutable */
    uint64_t _fireTSR;            /* TSR units */
    //優先級
    CFIndex _order;            /* immutable */
    //任務回調
    CFRunLoopTimerCallBack _callout;    /* immutable */
    //上下文
    CFRunLoopTimerContext _context;    /* immutable, except invalidation */
};
複製代碼

從上面的代碼能夠看出,timer是依賴於runloop的,並且有函數指針回調,那麼即可以在設定的時間點拋出回調執行任務。同時蘋果官方文檔也有提到CFRunLoopTimerNSTimertoll-free bridged的,這就一位着二者之間能夠相互轉換。 關於NSTimer和RunLoop之間的關係,能夠參照以前的文章iOS定時器-- NSTimer&GCD定時器

1.七、CFRunLoopObserverRef--觀察者

CFRunLoopObserver是觀察者,監測RunLoop的各類狀態的變化。以下是CFRunLoopObserver的源碼。

struct __CFRunLoopObserver {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;
    //對應的runLoop對象
    CFRunLoopRef _runLoop;
    // 當前的觀察的runloop個數
    CFIndex _rlCount;
    //runloop的狀態
    CFOptionFlags _activities;        /* immutable */
    CFIndex _order;            /* immutable */
    //回調
    CFRunLoopObserverCallBack _callout;    /* immutable */
    //上下文
    CFRunLoopObserverContext _context;    /* immutable, except invalidation */
};
複製代碼

RunLoop的source事件源來監測是否有須要執行的任務,而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
};
複製代碼

1.八、小結

  1. RunLoop是一種高級的循環機制,讓程序持續運行,並處理程序中的各類事件,讓線程在須要作事的時候忙起來,不須要的話就讓線程休眠
  2. RunLoop和線程是綁定在一塊兒的,每條線程都有惟一一個與之對應的RunLoop對象
  3. 每一個RunLoop對象都會包含有若干個mode,每一個mode包含有惟一一個name,若干個sources0事件,若干個sources1事件,若干個timer事件,若干個observer事件和若干portRunLoop老是在某種特定的mode下運行的,這個特定的mode即是_currentMode

二、RunLoop底層實現原理

2.一、RunLoop啓動

RunLoop啓動有兩個方法可供調用,分別是CFRunLoopRunCFRunLoopRunInMode。先來看一下這兩個方法的源碼。

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

SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
複製代碼

從上面這兩個方法的實現能夠看出,CFRunLoopRun方法啓動的RunLoop是運行在kCFRunLoopDefaultMode模式下的,即以這種方式啓動的runloop是在默認模式下運行的。而CFRunLoopRunInMode方法則是須要指定運行的mode。從這裏也能夠看出來RunLoop雖然有不少的mode,可是RunLoop的運行只能是在一種mode下進行。同時這兩個方法都調用了CFRunLoopRunSpecific方法,該方法就是具體啓動RunLoop的方法,這個方法的第一個參數就是當前的RunLoop,因此在分析CFRunLoopRunSpecific方法以前,先來看下是怎麼獲取到RunLoop的。

2.二、獲取RunLoop

蘋果開放給開發者連個方法獲取RunLoop對象,分別是CFRunLoopGetCurrentCFRunLoopGetMain,它們分別表明着獲取當前線程的Runloop對象和獲取主線程的RunLoop對象。

2.2.一、CFRunLoopGetCurrent

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

上面的代碼是CFRunLoopGetCurrent的實現源碼,從這段代碼能夠看出該方法內部調用了_CFRunLoopGet0方法,傳入的參數是當前線程pthread_self(),因而可知CFRunLoopGetCurrent函數必需要在線程內部調用才能獲取到RunLoop對象。

2.2.二、CFRunLoopGetMain

CFRunLoopRef CFRunLoopGetMain(void) {
    CHECK_FOR_FORK();
    static CFRunLoopRef __main = NULL; // no retain needed
    if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
    return __main;
}
複製代碼

上面的代碼是CFRunLoopGetMain的實現源碼,從這段代碼能夠看出該方法內部調用了_CFRunLoopGet0方法,傳入的參數是主線程pthread_main_thread_np(),因而可知CFRunLoopGetCurrent函數無論是在主線程中仍是在子線程中均可以獲取到主線程的RunLoop。

2.2.三、_CFRunLoopGet0

static CFMutableDictionaryRef __CFRunLoops = NULL;
static CFLock_t loopsLock = CFLockInit;

// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t)
{
    if (pthread_equal(t, kNilPthreadT)) {
        t = pthread_main_thread_np();
    }
    __CFLock(&loopsLock);
    if (!__CFRunLoops) {
        __CFUnlock(&loopsLock);
        //建立一個字典
        CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
        //建立主線程的RunLoop
        CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
        //把主線程的RunLoop保存到dict中,key是線程,value是RunLoop
        CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
        //此處NULL和__CFRunLoops指針都指向NULL,匹配,因此將dict寫到__CFRunLoops
        if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void *volatile *)&__CFRunLoops)) {
            CFRelease(dict);
        }
        //釋放主線程RunLoop
        CFRelease(mainLoop);
        __CFLock(&loopsLock);
    }
    // 根據線程從__CFRunLoops獲取RunLoop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    //若是在__CFRunLoops中沒有找到
    if (!loop) {
        //建立一個新的RunLoop
        CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
        loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        if (!loop) {
            //把新建立的RunLoop存放到__CFRunLoops中
            CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
            loop = newLoop;
        }
        // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
        __CFUnlock(&loopsLock);
        CFRelease(newLoop);
    }
    //若是傳入的線程就是當前的線程
    if (pthread_equal(t, pthread_self())) {
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            //註冊一個回調,噹噹前線程銷燬時銷燬對應的RunLoop
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS - 1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;
}
複製代碼

上面這段關於CFRunLoopGet0函數方法的代碼並不複雜,咱們能夠從中獲得以下幾個信息:

  1. RunLoop和線程是一一對應的,是以線程爲key,RunLoop對象爲value存放在一個全局字典中的。
  2. 主線程的RunLoop會在初始化全局化字典時建立。
  3. 子線程的RunLoop會在第一次獲取時建立。
  4. 當線程銷燬時,對應的RunLoop也會隨之銷燬。

2.三、CFRunLoopRunSpecific

讓咱們回到CFRunLoopRunSpecific方法。

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled)       /* DOES CALLOUT */
{
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
    __CFRunLoopLock(rl);
    //根據modeName找到本次運行的mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    //若是沒有找到mode或者找到的mode中沒有註冊事件則退出,不進入循環
    if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
        Boolean did = false;
        if (currentMode)
            __CFRunLoopModeUnlock(currentMode);
        __CFRunLoopUnlock(rl);
        return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
    }
    volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
    //取上一次運行的mode
    CFRunLoopModeRef previousMode = rl->_currentMode;
    rl->_currentMode = currentMode;
    int32_t result = kCFRunLoopRunFinished;
    //通知observer即將進入RunLoop
    if (currentMode->_observerMask & kCFRunLoopEntry)
        __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    //通知observer已經退出RunLoop
    if (currentMode->_observerMask & kCFRunLoopExit)
        __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

    __CFRunLoopModeUnlock(currentMode);
    __CFRunLoopPopPerRunData(rl, previousPerRun);
    rl->_currentMode = previousMode;
    __CFRunLoopUnlock(rl);
    return result;
}
複製代碼

如上是CFRunLoopRunSpecific方法的實現代碼,這段代碼看上去複雜,其實很簡單。這個方法須要傳入四個參數:

  • rl:當前運行的RunLoop對象。
  • modeName:指定RunLoop對象的mode的名稱。
  • seconds:RunLoop的超時時間
  • returnAfterSourceHandled:是否在處理完事件以後返回。

從上面的代碼咱們能夠獲取到以下幾點信息:

  1. RunLoop運行必需要指定一個mode,不然不會運行RunLoop。
  2. 若是指定的mode沒有註冊時間任務,RunLoop不會運行。
  3. 通知observer進入runloop,調用__CFRunLoopRun方法處理任務,通知observer退出runloop。

2.四、__CFRunLoopRun

以下是__CFRunLoopRun方法的源碼(摘自iOS RunLoop詳解,小編懶得爲代碼添加註釋☺️☺️)。

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    //獲取系統啓動後的CPU運行時間,用於控制超時時間
    uint64_t startTSR = mach_absolute_time();
    
    //若是RunLoop或者mode是stop狀態,則直接return,不進入循環
    if (__CFRunLoopIsStopped(rl)) {
        __CFRunLoopUnsetStopped(rl);
        return kCFRunLoopRunStopped;
    } else if (rlm->_stopped) {
        rlm->_stopped = false;
        return kCFRunLoopRunStopped;
    }
    
    //mach端口,在內核中,消息在端口之間傳遞。 初始爲0
    mach_port_name_t dispatchPort = MACH_PORT_NULL;
    //判斷是否爲主線程
    Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));
    //若是在主線程 && runloop是主線程的runloop && 該mode是commonMode,則給mach端口賦值爲主線程收發消息的端口
    if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) dispatchPort = _dispatch_get_main_queue_port_4CF();
    
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    mach_port_name_t modeQueuePort = MACH_PORT_NULL;
    if (rlm->_queue) {
        //mode賦值爲dispatch端口_dispatch_runloop_root_queue_perform_4CF
        modeQueuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue);
        if (!modeQueuePort) {
            CRASH("Unable to get port for run loop mode queue (%d)", -1);
        }
    }
#endif
    
    //GCD管理的定時器,用於實現runloop超時機制
    dispatch_source_t timeout_timer = NULL;
    struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context));
    
    //當即超時
    if (seconds <= 0.0) { // instant timeout
        seconds = 0.0;
        timeout_context->termTSR = 0ULL;
    }
    //seconds爲超時時間,超時時執行__CFRunLoopTimeout函數
    else if (seconds <= TIMER_INTERVAL_LIMIT) {
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, DISPATCH_QUEUE_OVERCOMMIT);
        timeout_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
        dispatch_retain(timeout_timer);
        timeout_context->ds = timeout_timer;
        timeout_context->rl = (CFRunLoopRef)CFRetain(rl);
        timeout_context->termTSR = startTSR + __CFTimeIntervalToTSR(seconds);
        dispatch_set_context(timeout_timer, timeout_context); // source gets ownership of context
        dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout);
        dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel);
        uint64_t ns_at = (uint64_t)((__CFTSRToTimeInterval(startTSR) + seconds) * 1000000000ULL);
        dispatch_source_set_timer(timeout_timer, dispatch_time(1, ns_at), DISPATCH_TIME_FOREVER, 1000ULL);
        dispatch_resume(timeout_timer);
    }
    //永不超時
    else { // infinite timeout
        seconds = 9999999999.0;
        timeout_context->termTSR = UINT64_MAX;
    }
    
    //標誌位默認爲true
    Boolean didDispatchPortLastTime = true;
    //記錄最後runloop狀態,用於return
    int32_t retVal = 0;
    do {
        //初始化一個存放內核消息的緩衝池
        uint8_t msg_buffer[3 * 1024];
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        mach_msg_header_t *msg = NULL;
        mach_port_t livePort = MACH_PORT_NULL;
#elif DEPLOYMENT_TARGET_WINDOWS
        HANDLE livePort = NULL;
        Boolean windowsMessageReceived = false;
#endif
        //取全部須要監聽的port
        __CFPortSet waitSet = rlm->_portSet;
        
        //設置RunLoop爲能夠被喚醒狀態
        __CFRunLoopUnsetIgnoreWakeUps(rl);
        
        //2.通知observer,即將觸發timer回調,處理timer事件
        if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        //3.通知observer,即將觸發Source0回調
        if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
        
        //執行加入當前runloop的block
        __CFRunLoopDoBlocks(rl, rlm);
        
        //4.處理source0事件
        //有事件處理返回true,沒有事件返回false
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        if (sourceHandledThisLoop) {
            //執行加入當前runloop的block
            __CFRunLoopDoBlocks(rl, rlm);
        }
        
        //若是沒有Sources0事件處理 而且 沒有超時,poll爲false
        //若是有Sources0事件處理 或者 超時,poll都爲true
        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
        
        //第一次do..whil循環不會走該分支,由於didDispatchPortLastTime初始化是true
        if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
            //從緩衝區讀取消息
            msg = (mach_msg_header_t *)msg_buffer;
            //5.接收dispatchPort端口的消息,(接收source1事件)
            if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0)) {
                //若是接收到了消息的話,前往第9步開始處理msg
                goto handle_msg;
            }
#elif DEPLOYMENT_TARGET_WINDOWS
            if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) {
                goto handle_msg;
            }
#endif
        }
        
        didDispatchPortLastTime = false;
        
        //6.通知觀察者RunLoop即將進入休眠
        if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        //設置RunLoop爲休眠狀態
        __CFRunLoopSetSleeping(rl);
        // do not do any user callouts after this point (after notifying of sleeping)
        
        // Must push the local-to-this-activation ports in on every loop
        // iteration, as this mode could be run re-entrantly and we don't
        // want these ports to get serviced.
        
        __CFPortSetInsert(dispatchPort, waitSet);
        
        __CFRunLoopModeUnlock(rlm);
        __CFRunLoopUnlock(rl);
        
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
#if USE_DISPATCH_SOURCE_FOR_TIMERS
        //這裏有個內循環,用於接收等待端口的消息
        //進入此循環後,線程進入休眠,直到收到新消息才跳出該循環,繼續執行run loop
        do {
            if (kCFUseCollectableAllocator) {
                objc_clear_stack(0);
                memset(msg_buffer, 0, sizeof(msg_buffer));
            }
            msg = (mach_msg_header_t *)msg_buffer;
            //7.接收waitSet端口的消息
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
            //收到消息以後,livePort的值爲msg->msgh_local_port,
            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);
#else
        if (kCFUseCollectableAllocator) {
            objc_clear_stack(0);
            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);
#endif
        
        
#elif DEPLOYMENT_TARGET_WINDOWS
        // Here, use the app-supplied message queue mask. They will set this if they are interested in having this run loop receive windows messages.
        __CFRunLoopWaitForMultipleObjects(waitSet, NULL, poll ? 0 : TIMEOUT_INFINITY, rlm->_msgQMask, &livePort, &windowsMessageReceived);
#endif
        
        __CFRunLoopLock(rl);
        __CFRunLoopModeLock(rlm);
        
        // Must remove the local-to-this-activation ports in on every loop
        // iteration, as this mode could be run re-entrantly and we don't
        // want these ports to get serviced. Also, we don't want them left
        // in there if this function returns.
        
        __CFPortSetRemove(dispatchPort, waitSet);
        
 
        __CFRunLoopSetIgnoreWakeUps(rl);
        
        // user callouts now OK again
        //取消runloop的休眠狀態
        __CFRunLoopUnsetSleeping(rl);
        //8.通知觀察者runloop被喚醒
        if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
      
        //9.處理收到的消息
    handle_msg:;
        __CFRunLoopSetIgnoreWakeUps(rl);
        
#if DEPLOYMENT_TARGET_WINDOWS
        if (windowsMessageReceived) {
            // These Win32 APIs cause a callout, so make sure we're unlocked first and relocked after
            __CFRunLoopModeUnlock(rlm);
            __CFRunLoopUnlock(rl);
            
            if (rlm->_msgPump) {
                rlm->_msgPump();
            } else {
                MSG msg;
                if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE | PM_NOYIELD)) {
                    TranslateMessage(&msg);
                    DispatchMessage(&msg);
                }
            }
            
            __CFRunLoopLock(rl);
            __CFRunLoopModeLock(rlm);
            sourceHandledThisLoop = true;
            
            // To prevent starvation of sources other than the message queue, we check again to see if any other sources need to be serviced
            // Use 0 for the mask so windows messages are ignored this time. Also use 0 for the timeout, because we're just checking to see if the things are signalled right now -- we will wait on them again later.
            // NOTE: Ignore the dispatch source (it's not in the wait set anymore) and also don't run the observers here since we are polling.
            __CFRunLoopSetSleeping(rl);
            __CFRunLoopModeUnlock(rlm);
            __CFRunLoopUnlock(rl);
            
            __CFRunLoopWaitForMultipleObjects(waitSet, NULL, 0, 0, &livePort, NULL);
            
            __CFRunLoopLock(rl);
            __CFRunLoopModeLock(rlm);
            __CFRunLoopUnsetSleeping(rl);
            // If we have a new live port then it will be handled below as normal
        }
        
        
#endif
        if (MACH_PORT_NULL == livePort) {
            CFRUNLOOP_WAKEUP_FOR_NOTHING();
            // handle nothing
            //經過CFRunloopWake喚醒
        } else if (livePort == rl->_wakeUpPort) {
            CFRUNLOOP_WAKEUP_FOR_WAKEUP();
            //什麼都不幹,跳回2從新循環
            // do nothing on Mac OS
#if DEPLOYMENT_TARGET_WINDOWS
            // Always reset the wake up port, or risk spinning forever
            ResetEvent(rl->_wakeUpPort);
#endif
        }
#if USE_DISPATCH_SOURCE_FOR_TIMERS
        //若是是定時器事件
        else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
            CFRUNLOOP_WAKEUP_FOR_TIMER();
            //9.1 處理timer事件
            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                // Re-arm the next timer, because we apparently fired early
                __CFArmNextTimerInMode(rlm, rl);
            }
        }
#endif
#if USE_MK_TIMER_TOO
        //若是是定時器事件
        else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
            CFRUNLOOP_WAKEUP_FOR_TIMER();
            // On Windows, we have observed an issue where the timer port is set before the time which we requested it to be set. For example, we set the fire time to be TSR 167646765860, but it is actually observed firing at TSR 167646764145, which is 1715 ticks early. The result is that, when __CFRunLoopDoTimers checks to see if any of the run loop timers should be firing, it appears to be 'too early' for the next timer, and no timers are handled.
            // In this case, the timer port has been automatically reset (since it was returned from MsgWaitForMultipleObjectsEx), and if we do not re-arm it, then no timers will ever be serviced again unless something adjusts the timer list (e.g. adding or removing timers). The fix for the issue is to reset the timer here if CFRunLoopDoTimers did not handle a timer itself. 9308754
           //9.1處理timer事件
            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                // Re-arm the next timer
                __CFArmNextTimerInMode(rlm, rl);
            }
        }
#endif
        //若是是dispatch到main queue的block
        else if (livePort == dispatchPort) {
            CFRUNLOOP_WAKEUP_FOR_DISPATCH();
            __CFRunLoopModeUnlock(rlm);
            __CFRunLoopUnlock(rl);
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);
#if DEPLOYMENT_TARGET_WINDOWS
            void *msg = 0;
#endif
            //9.2執行block
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);
            __CFRunLoopLock(rl);
            __CFRunLoopModeLock(rlm);
            sourceHandledThisLoop = true;
            didDispatchPortLastTime = true;
        } else {
            CFRUNLOOP_WAKEUP_FOR_SOURCE();
            // Despite the name, this works for windows handles as well
            CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
            // 有source1事件待處理
            if (rls) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
                mach_msg_header_t *reply = NULL;
                //9.2 處理source1事件
                sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
                if (NULL != reply) {
                    (void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
                    CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply);
                }
#elif DEPLOYMENT_TARGET_WINDOWS
                sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls) || sourceHandledThisLoop;
#endif
            }
        }
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
#endif
        
        __CFRunLoopDoBlocks(rl, rlm);
        
        if (sourceHandledThisLoop && stopAfterHandle) {
            //進入run loop時傳入的參數,處理完事件就返回
            retVal = kCFRunLoopRunHandledSource;
        }else if (timeout_context->termTSR < mach_absolute_time()) {
            //run loop超時
            retVal = kCFRunLoopRunTimedOut;
        }else if (__CFRunLoopIsStopped(rl)) {
            //run loop被手動終止
            __CFRunLoopUnsetStopped(rl);
            retVal = kCFRunLoopRunStopped;
        }else if (rlm->_stopped) {
            //mode被終止
            rlm->_stopped = false;
            retVal = kCFRunLoopRunStopped;
        }else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
            //mode中沒有要處理的事件
            retVal = kCFRunLoopRunFinished;
        }
        //除了上面這幾種狀況,都繼續循環
    } while (0 == retVal);
    
    if (timeout_timer) {
        dispatch_source_cancel(timeout_timer);
        dispatch_release(timeout_timer);
    } else {
        free(timeout_context);
    }
    
    return retVal;
}
複製代碼

__CFRunLoopRun方法的源碼很長,看上出寫了一堆亂七八糟的東西,實際上該方法內部就是一個 do-while 循環,當調用該方法時,線程就會一直留在這個循環裏面,直到超時或者手動被中止,該方法纔會返回。在這裏循環裏面,線程在空閒的時候處於休眠狀態,在有事件須要處理的時候,處理事件。該方法是整個RunLoop運行的核心方法。蘋果官方文檔對於RunLoop處理各種事件的流程有着詳細的描述。

  1. 通知觀察者RunLoop已經啓動。
  2. 通知觀察者定時器即將觸發。
  3. 通知觀察者任何不基於端口的輸入源都將觸發。
  4. 觸發全部準備觸發的非基於端口的輸入源。
  5. 若是基於端口的輸入源已準備好並等待啓動,當即處理事件;並進入步驟9。
  6. 通知觀察者線程進入休眠狀態。
  7. 使線程進入睡眠狀態,直到發生如下事件之一:
    • 某一事件到達基於端口的源。
    • 定時器觸發。
    • RunLoop設置的時間已經超時。
    • RunLoop被喚醒。
  8. 通知觀察者線程即將被喚醒。
  9. 處理未處理的事件。
    • 若是用戶定義的定時器啓動,處理定時器事件並重啓RunLoop。進入步驟2。
    • 若是輸入源啓動,傳遞相應的消息。
    • 若是RunLoop被喚醒並且時間還沒超時,重啓RunLoop。進入步驟2。
  10. 通知觀察者RunLoop結束。

整個流程以下圖所示:

2.五、__CFRunLoopServiceMachPort

若是你仔細看過__CFRunLoopRun方法的代碼實現,就會發如今其方法內部有一個內置的循環,這個循環會讓線程進入休眠狀態,直到收到新消息才跳出該循環,繼續執行RunLoop。這些消息是基於mach port來進行進程之間的通信的。

static Boolean __CFRunLoopServiceMachPort(mach_port_name_t port, mach_msg_header_t **buffer, size_t buffer_size, mach_port_t *livePort, mach_msg_timeout_t timeout) {
    Boolean originalBuffer = true;
    kern_return_t ret = KERN_SUCCESS;
    for (;;) {      /* In that sleep of death what nightmares may come ... */
        mach_msg_header_t *msg = (mach_msg_header_t *)*buffer;
        msg->msgh_bits = 0;  //消息頭的標誌位
        msg->msgh_local_port = port;  //源(發出的消息)或者目標(接收的消息)
        msg->msgh_remote_port = MACH_PORT_NULL; //目標(發出的消息)或者源(接收的消息)
        msg->msgh_size = buffer_size;  //消息緩衝區大小,單位是字節
        msg->msgh_id = 0;  //惟一id
       
        if (TIMEOUT_INFINITY == timeout) { CFRUNLOOP_SLEEP(); } else { CFRUNLOOP_POLL(); }
        
        //經過mach_msg發送或者接收的消息都是指針,
        //若是直接發送或者接收消息體,會頻繁進行內存複製,損耗性能
        //因此XNU使用了單一內核的方式來解決該問題,全部內核組件都共享同一個地址空間,所以傳遞消息時候只須要傳遞消息的指針
        ret = mach_msg(msg,
                       MACH_RCV_MSG|MACH_RCV_LARGE|((TIMEOUT_INFINITY != timeout) ? MACH_RCV_TIMEOUT : 0)|MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0)|MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AV),
                       0,
                       msg->msgh_size,
                       port,
                       timeout,
                       MACH_PORT_NULL);
        CFRUNLOOP_WAKEUP(ret);
        
        //接收/發送消息成功,給livePort賦值爲msgh_local_port
        if (MACH_MSG_SUCCESS == ret) {
            *livePort = msg ? msg->msgh_local_port : MACH_PORT_NULL;
            return true;
        }
        
        //MACH_RCV_TIMEOUT
        //超出timeout時間沒有收到消息,返回MACH_RCV_TIMED_OUT
        //此時釋放緩衝區,把livePort賦值爲MACH_PORT_NULL
        if (MACH_RCV_TIMED_OUT == ret) {
            if (!originalBuffer) free(msg);
            *buffer = NULL;
            *livePort = MACH_PORT_NULL;
            return false;
        }
        
        //MACH_RCV_LARGE
        //若是接收緩衝區過小,則將過大的消息放在隊列中,而且出錯返回MACH_RCV_TOO_LARGE,
        //這種狀況下,只返回消息頭,調用者能夠分配更多的內存
        if (MACH_RCV_TOO_LARGE != ret) break;
        //此處給buffer分配更大內存
        buffer_size = round_msg(msg->msgh_size + MAX_TRAILER_SIZE);
        if (originalBuffer) *buffer = NULL;
        originalBuffer = false;
        *buffer = realloc(*buffer, buffer_size);
    }
    HALT;
    return false;
}
複製代碼

如上是__CFRunLoopServiceMachPort的源碼,該方法接收指定內核端口的消息,並將消息緩存在緩存區,供外界獲取。該方法的核心是mach_msg方法,該方法實現消息的發送個接收。RunLoop調用這個函數去接收消息,若是沒有接收到port的消息,內核會將線程置於等待狀態。

2.六、RunLoop事件處理

在上一個小節中咱們探索了RunLoop運行的核心方法__CFRunLoopRun的代碼,根據官方文檔的描述總結了事件處理的流程。源碼中顯示處理事件主要涉及到以下幾個方法:

  • __CFRunLoopDoObservers:處理通知事件。
  • __CFRunLoopDoBlocks:處理block事件。
  • __CFRunLoopDoSources0:處理source0事件。
  • __CFRunLoopDoSource1:處理source1事件。
  • __CFRunLoopDoTimers:處理定時器。
  • CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE:GCD主隊列

這些方法的實現咱們沒必要關係,可是這些方法在處理事件後如何回調給上層,纔是咱們須要關心的。好比說__CFRunLoopDoSources0處理的是系統的事件,那麼觸發一個UIButton的點擊事件後,查看函數調用棧應該能夠知道回到給上層是如何進行的。

如上圖所示UIButton的點擊事件的函數調用棧,咱們能夠清楚的看到 __CFRunLoopDoSources0方法的調用,而後調用 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__回到UIKit層。

關於上述方法回調上層的方法對應以下圖所示:

2.七、小結

  1. RunLoop的運行一定要指定一種mode,而且該mode必須註冊任務事件。
  2. RunLoop是在默認mode下運行的,固然也能夠指定一種mode運行,可是隻能在一種mode下運行。
  3. RunLoop內部其實是維護了一個do-while循環,線程就會一直留在這個循環裏面,直到超時或者手動被中止。
  4. RunLoop 的核心就是一個 mach_msg() ,RunLoop 調用這個函數去接收消息,若是沒有別人發送 port 消息過來,內核會將線程置於等待狀態,不然線程處理事件。

三、RunLoop應用

3.一、NSTimer

用過NSTimer來作定時器開發的人都知道,NSTimer對象須要添加到RunLoop中才能正確執行。在前面的內容也講到了CFRunLoopTimerNSTimertoll-free bridged的。一個NSTimer註冊到 RunLoop 後,RunLoop會爲其重複的時間點註冊好事件。尤爲是在一個滾動視圖中使用NSTimer時,因爲其mode的變化致使NSTimer中止工做,解決這個問題的關鍵就是講NSTimer註冊到RunLoopNSRunLoopCommonModes下。NSTimer更多的內容請移步iOS定時器-- NSTimer&GCD定時器

GCD則不一樣,GCD的線程管理是經過系統來直接管理的。GCD Timer是經過dispatch portRunLoop發送消息,來使RunLoop執行相應的block,若是所在線程沒有RunLoop,那麼GCD 會臨時建立一個線程去執行block,執行完以後再銷燬掉,所以GCDTimer是不依賴RunLoop的。

3.二、AutoReleasepool

通常不多會將自動釋放池和RunLoop聯繫起來,可是若是打印[NSRunLoop currentRunLoop]結果中會發現和自動釋放池相關的回調。

<CFRunLoopObserver 0x6000024246e0 [0x7fff8062ce20]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff48c1235c), context = <CFArray 0x600001b7afd0 [0x7fff8062ce20]>{type = mutable-small, count = 1, values = (0 : <0x7fc18f80e038>)}}
<CFRunLoopObserver 0x600002424640 [0x7fff8062ce20]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff48c1235c), context = <CFArray 0x600001b7afd0 [0x7fff8062ce20]>{type = mutable-small, count = 1, values = (0 : <0x7fc18f80e038>)}}
複製代碼

即App啓動後,蘋果會給RunLoop註冊不少個observers,其中有兩個是跟自動釋放池相關的,其回調都是_wrapRunLoopWithAutoreleasePoolHandler()。\

  • 第一個observer監聽的是activities=0x1(kCFRunLoopEntry),也就是在即將進入loop時,其回調會調用_objc_autoreleasePoolPush() 建立自動釋放池;
  • 第二個observer監聽的是activities = 0xa0(kCFRunLoopBeforeWaiting | kCFRunLoopExit), 即監聽的是準備進入睡眠和即將退出loop兩個事件。在準備進入睡眠以前,由於睡眠可能時間很長,因此爲了避免佔用資源先調用_objc_autoreleasePoolPop()釋放舊的釋放池,並調用_objc_autoreleasePoolPush() 建立新建一個新的,用來裝載被喚醒後要處理的事件對象;在最後即將退出loop時則會 _objc_autoreleasePoolPop()釋放池子。

關於自動釋放池更多的內容請移步iOS內存管理二:自動釋放池autoreleasepool

3.三、卡頓檢測

咱們能夠經過RunLoop的不一樣狀態來作頁面刷新的卡頓檢測。

- (void)start{
    [self registerObserver];
    [self startMonitor];
}

static void CallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    DSBlockMonitor *monitor = (__bridge DSBlockMonitor *)info;
    monitor->activity = activity;
    // 發送信號
    dispatch_semaphore_t semaphore = monitor->_semaphore;
    dispatch_semaphore_signal(semaphore);
}

- (void)registerObserver{
    CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
    //NSIntegerMax : 優先級最小
    CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                                            kCFRunLoopAllActivities,
                                                            YES,
                                                            NSIntegerMax,
                                                            &CallBack,
                                                            &context);
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}

- (void)startMonitor{
    // 建立信號
    _semaphore = dispatch_semaphore_create(0);
    // 在子線程監控時長
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (YES)
        {
            //超時時間是 1 秒,沒有等到信號量,st 就不等於 0, RunLoop 全部的任務
            long st = dispatch_semaphore_wait(self->_semaphore, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC));
            if (st != 0)
            {
                if (self->activity == kCFRunLoopBeforeSources || self->activity == kCFRunLoopAfterWaiting)
                {
                    if (++self->_timeoutCount < 2){
                        NSLog(@"timeoutCount==%lu",(unsigned long)self->_timeoutCount);
                        continue;
                    }
                    NSLog(@"檢測到卡頓");
                }
            }
            self->_timeoutCount = 0;
        }
    });
}

複製代碼

如上面的代碼所示就是一段簡單的監測頁面卡頓的代碼,其原理就是根據RunLoop進入kCFRunLoopBeforeSources狀態處理source事件到kCFRunLoopAfterWaiting狀態變化之間的時間間隔來作判斷依據。固然這只是一個簡單的demo,可是RunLoop各類狀態的變化爲不少優秀的卡頓檢測的三方庫提供了理論基礎。

3.四、常駐線程

有的時候咱們須要建立一個線程在後臺一直作一些任務,可是常規的線程在任務完成後就會當即銷燬,所以咱們須要一個常駐線程來讓線程一直都存在。

- (void)viewDidLoad {
    [super viewDidLoad];
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [self.thread start];
}

- (void)run {
    NSRunLoop *currentRl = [NSRunLoop currentRunLoop];
    [currentRl addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    [currentRl run];
}

- (void)run2
{
    NSLog(@"常駐線程");
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:NO];
}
複製代碼

上面的代碼就是一個常駐線程。把線程thread添加在到RunLoop中,一般RunLoop啓動前必需要設置一個mode,而且爲mode至少設置一個Source/Timer/Observer,在這裏是添加了一個port,雖然消息能夠經過port發送到RunLoop內,可是這裏並無發送任何的消息,因此這樣即可以保持RunLoop不退出,s實現線程常駐。

參考資料

相關文章
相關標籤/搜索