iOS:RunLoop詳解

1、RunLoop初探

1.1、RunLoop是什么?

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

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.

這段話翻譯成中文如下:

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

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

1.2、RunLoop與線程

從上面關于RunLoop的定義我們可以知道,RunLoop和線程有著密不可分的關系。通常情況下線程的作用是用來執行一個或多個特定的任務,在線程執行完成之后就會退出不再執行任務,RunLoop這樣的循環機制會讓線程能夠不斷地執行任務并不退出。

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。

從上面這一點話中我們獲取到如下幾點信息:

  • RunLoop和線程是綁定在一起的,每條線程都有唯一一個與之對應的RunLoop對象。
  • 不能自己創建RunLoop對象,但是可以獲取系統提供的RunLoop對象
  • 主線程的RunLoop對象是由系統自動創建好的,在應用程序啟動的時候會自動完成啟動,而子線程中的RunLoop對象需要我們手動獲取并啟動。

RunLoop與線程的關系如下圖所示:

image

從上圖中可以看出,RunLoop在線程中不斷檢測,通過input sourcetimer source接受事件,然后通知線程進行處理事件。

1.3、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.4、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,那么就形成了如下如所示的結構。

image

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

  • NSDefaultRunLoopMode:默認模式是用于大多數操作的模式。大多數時候使用此模式來啟動RunLoop并配置輸入源。
  • NSConnectionReplyMode:Cocoa將此模式與NSConnection對象結合使用以監測回應。幾乎不需要自己使用此模式。
  • NSModalPanelRunLoopMode:Cocoa使用此模式來識別用于模式面板的事件。
  • NSEventTrackingRunLoopMode:Cocoa使用此模式來限制鼠標拖動loop和其他類型的用戶界面跟蹤loop期間的傳入事件。通常用不到。
  • NSRunLoopCommonModes:是NSDefaultRunLoopModeNSEventTrackingRunLoopMode集合,在這種模式下RunLoop分別注冊了NSDefaultRunLoopModeUITrackingRunLoopMode。當然也可以通過調用CFRunLoopAddCommonMode()方法將自定義Mode放到kCFRunLoopCommonModes組合。

1.5、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.6、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.7、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.8、小結

  1. RunLoop是一種高級的循環機制,讓程序持續運行,并處理程序中的各種事件,讓線程在需要做事的時候忙起來,不需要的話就讓線程休眠
  2. RunLoop和線程是綁定在一起的,每條線程都有唯一一個與之對應的RunLoop對象
  3. 每個RunLoop對象都會包含有若干個mode,每個mode包含有唯一一個name,若干個sources0事件,若干個sources1事件,若干個timer事件,若干個observer事件和若干port,RunLoop總是在某種特定的mode下運行的,這個特定的mode便是_currentMode
image

2、RunLoop底層實現原理

2.1、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.2、獲取RunLoop

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

2.2.1、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.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.3、_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.3、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.4、__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結束。

整個流程如下圖所示:


image

2.5、__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.6、RunLoop事件處理

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

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

這些方法的實現我們不必關系,但是這些方法在處理事件后如何回調給上層,才是我們需要關心的。比如說__CFRunLoopDoSources0處理的是系統的事件,那么觸發一個UIButton的點擊事件后,查看函數調用棧應該可以知道回到給上層是如何進行的。

image

如上圖所示UIButton的點擊事件的函數調用棧,我們可以清楚的看到__CFRunLoopDoSources0方法的調用,然后調用__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__回到UIKit層。

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


image

2.7、小結

  1. RunLoop的運行必定要指定一種mode,并且該mode必須注冊任務事件。
  2. RunLoop是在默認mode下運行的,當然也可以指定一種mode運行,但是只能在一種mode下運行。
  3. RunLoop內部實際上是維護了一個do-while循環,線程就會一直留在這個循環里面,直到超時或者手動被停止。
  4. RunLoop 的核心就是一個 mach_msg() ,RunLoop 調用這個函數去接收消息,如果沒有別人發送 port 消息過來,內核會將線程置于等待狀態,否則線程處理事件。

3、RunLoop應用

3.1、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.2、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.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.4、常駐線程

有的時候我們需要創建一個線程在后臺一直做一些任務,但是常規的線程在任務完成后就會立即銷毀,因此我們需要一個常駐線程來讓線程一直都存在。

- (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實現線程常駐。

參考資料

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內容

  • 一、概述 一般來說,一個線程只能執行一個任務,執行完就會退出,如果我們需要一種機制,讓線程能隨時處理時間但并不退出...
    一直在路上66閱讀 293評論 0 0
  • ios RunLoop詳解 一、概述 一般來說,一個線程只能執行一個任務,執行完就會退出。如果我們需要一種機制,讓...
    721e472431a4閱讀 812評論 0 1
  • RunLoop簡介 從字面意思來看是運行循環,在程序運行過程中循環做一些事情,如果沒有Runloop程序執行完畢就...
    一直很安靜_25ae閱讀 411評論 0 0
  • 二、runloop應用 2.1 NSTimer 前面一直提到Timer Source作為事件源,事實上它的上層對應...
    leonardni閱讀 3,055評論 0 3
  • 一.RunLoop介紹 1.概念 RunLoop是一個運行循環,正是因為RunLoop,IOS才可以保持程序的持續...
    一片姜汁閱讀 391評論 0 0