iOS:RunLoop詳解

1、RunLoop初探

1.1鹏氧、RunLoop是什么飒筑?

RunLoop從字面上來說是跑圈的意思片吊,如果這樣理解不免有些膚淺。下面是蘋果官方文檔的關(guān)于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是與線程息息相關(guān)的基本基礎(chǔ)結(jié)構(gòu)的一部分俏脊。RunLoop是一個調(diào)度任務(wù)和處理任務(wù)的事件循環(huán)。RunLoop的目的是為了在有工作的時讓線程忙起來肤晓,而在沒工作時讓線程進(jìn)入睡眠狀態(tài)爷贫。

簡單的說RunLoop是一種高級的循環(huán)機(jī)制,讓程序持續(xù)運(yùn)行补憾,并處理程序中的各種事件漫萄,讓線程在需要做事的時候忙起來,不需要的話就讓線程休眠盈匾。

1.2腾务、RunLoop與線程

從上面關(guān)于RunLoop的定義我們可以知道,RunLoop和線程有著密不可分的關(guān)系削饵。通常情況下線程的作用是用來執(zhí)行一個或多個特定的任務(wù)岩瘦,在線程執(zhí)行完成之后就會退出不再執(zhí)行任務(wù),RunLoop這樣的循環(huán)機(jī)制會讓線程能夠不斷地執(zhí)行任務(wù)并不退出窿撬。

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管理并不是完全自動的启昧。需要設(shè)計一個線程在合適的時機(jī)啟動并響應(yīng)傳入的事件,您仍然必須設(shè)計線程的代碼以在適當(dāng)?shù)臅r候啟動運(yùn)行循環(huán)并響應(yīng)傳入的事件劈伴。 CocoaCore Foundation都提供RunLoop對象密末,以幫助配置和管理線程的RunLoop。應(yīng)用程序并不需要顯式創(chuàng)建這些對象。每個線程(包括應(yīng)用程序的主線程)都有一個關(guān)聯(lián)的RunLoop對象严里。但是新啼,在子線程中需要顯式地運(yùn)行RunLoop。在應(yīng)用程序啟動過程中田炭,應(yīng)用程序框架會自動在主線程上設(shè)置并運(yùn)行RunLoop师抄。

從上面這一點(diǎn)話中我們獲取到如下幾點(diǎn)信息:

  • RunLoop和線程是綁定在一起的漓柑,每條線程都有唯一一個與之對應(yīng)的RunLoop對象教硫。
  • 不能自己創(chuàng)建RunLoop對象,但是可以獲取系統(tǒng)提供的RunLoop對象辆布。
  • 主線程的RunLoop對象是由系統(tǒng)自動創(chuàng)建好的瞬矩,在應(yīng)用程序啟動的時候會自動完成啟動,而子線程中的RunLoop對象需要我們手動獲取并啟動锋玲。

RunLoop與線程的關(guān)系如下圖所示:

image

從上圖中可以看出景用,RunLoop在線程中不斷檢測,通過input sourcetimer source接受事件惭蹂,然后通知線程進(jìn)行處理事件伞插。

1.3、RunLoop的結(jié)構(gòu)

如下是RunLoop的結(jié)構(gòu)定義源碼:

struct __CFRunLoop {
    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所對應(yīng)的線程
    pthread_t _pthread;
    uint32_t _winthread;
    //標(biāo)記為common的mode的集合
    CFMutableSetRef _commonModes;
    //commonMode的item集合
    CFMutableSetRef _commonModeItems;
    //當(dāng)前的mode
    CFRunLoopModeRef _currentMode;
    //存儲的是CFRunLoopModeRef
    CFMutableSetRef _modes;
     // _block_item鏈表表頭指針
    struct _block_item *_blocks_head;
    // _block_item鏈表表尾指針
    struct _block_item *_blocks_tail;
    //運(yùn)行時間點(diǎn)
    CFAbsoluteTime _runTime;
    //睡眠時間點(diǎn)
    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;
    //保存所有需要監(jiān)聽的port芥玉,比如_wakeUpPort蛇摸,_timerPort都保存在這個數(shù)組中
    __CFPortSet _portSet;
    CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    //GCD定時器
    dispatch_source_t _timerSource;
    //GCD隊列
    dispatch_queue_t _queue;
    // 當(dāng)定時器觸發(fā)時設(shè)置為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事件和若干port弃鸦,RunLoop總是在某種特定的CFRunLoopMode下運(yùn)行的,這個特定的mode便是_currentMode。而CFRunloopRef對應(yīng)結(jié)構(gòu)體的定義知道一個RunLoop對象包含有若干個mode幢痘,那么就形成了如下如所示的結(jié)構(gòu)唬格。

image

關(guān)于CFRunLoopMode,蘋果提到了5個Model,分別是NSDefaultRunLoopMode购岗、NSConnectionReplyMode
汰聋、NSModalPanelRunLoopModeNSEventTrackingRunLoopMode喊积、NSRunLoopCommonModes烹困。在iOS中公開暴露只有NSDefaultRunLoopModeNSRunLoopCommonModes

  • NSDefaultRunLoopMode:默認(rèn)模式是用于大多數(shù)操作的模式乾吻。大多數(shù)時候使用此模式來啟動RunLoop并配置輸入源髓梅。
  • NSConnectionReplyMode:Cocoa將此模式與NSConnection對象結(jié)合使用以監(jiān)測回應(yīng)。幾乎不需要自己使用此模式绎签。
  • NSModalPanelRunLoopMode:Cocoa使用此模式來識別用于模式面板的事件枯饿。
  • NSEventTrackingRunLoopMode:Cocoa使用此模式來限制鼠標(biāo)拖動loop和其他類型的用戶界面跟蹤loop期間的傳入事件。通常用不到诡必。
  • NSRunLoopCommonModes:是NSDefaultRunLoopModeNSEventTrackingRunLoopMode集合奢方,在這種模式下RunLoop分別注冊了NSDefaultRunLoopModeUITrackingRunLoopMode。當(dāng)然也可以通過調(diào)用CFRunLoopAddCommonMode()方法將自定義Mode放到kCFRunLoopCommonModes組合爸舒。

1.5蟋字、CFRunLoopSourceRef-事件源

如下是CFRunLoopSource的源碼。

struct __CFRunLoopSource {
    CFRuntimeBase _base;
    //用于標(biāo)記Signaled狀態(tài)扭勉,source0只有在被標(biāo)記為Signaled狀態(tài)鹊奖,才會被處理
    uint32_t _bits;
    pthread_mutex_t _lock;
    CFIndex _order;            /* immutable */
    CFMutableBagRef _runLoops;
    //聯(lián)合體 
    union {
        CFRunLoopSourceContext version0;    /* immutable, except invalidation */
        CFRunLoopSourceContext1 version1;    /* immutable, except invalidation */
    } _context;
};

根據(jù)蘋果官方的定義CFRunLoopSource是輸入源的抽象,分為source0和source1兩個版本剖效。

  • source0:是App內(nèi)部事件嫉入,只包含一個函數(shù)指針回調(diào),并不能主動觸發(fā)事件璧尸,使用時咒林,你需要先調(diào)用CFRunLoopSourceSignal(source),將這個source標(biāo)記為待處理爷光,然后手動調(diào)用CFRunLoopWakeUp(runloop)來喚醒RunLoop垫竞,讓其處理這個事件。
  • source1source1包含一個mach_port和一個函數(shù)回調(diào)指針蛀序。source1是基于port的欢瞪,通過讀取某個port上內(nèi)核消息隊列上的消息來決定執(zhí)行的任務(wù),然后再分發(fā)到sources0中處理的徐裸。source1只供系統(tǒng)使用遣鼓,并不對開發(fā)者開放。

1.6重贺、CFRunLoopTimerRef--Timer事件

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

struct __CFRunLoopTimer {
    CFRuntimeBase _base;
    uint16_t _bits;
    pthread_mutex_t _lock;
    //timer對應(yīng)的runloop
    CFRunLoopRef _runLoop;
    //timer對應(yīng)的mode
    CFMutableSetRef _rlModes;
    //下一次觸發(fā)的時間
    CFAbsoluteTime _nextFireDate;
    //定時的間隔
    CFTimeInterval _interval;        /* immutable */
    //定時器允許的誤差
    CFTimeInterval _tolerance;          /* mutable */
    uint64_t _fireTSR;            /* TSR units */
    //優(yōu)先級
    CFIndex _order;            /* immutable */
    //任務(wù)回調(diào)
    CFRunLoopTimerCallBack _callout;    /* immutable */
    //上下文
    CFRunLoopTimerContext _context;    /* immutable, except invalidation */
};

從上面的代碼可以看出回懦,timer是依賴于runloop的,而且有函數(shù)指針回調(diào)次企,那么便可以在設(shè)定的時間點(diǎn)拋出回調(diào)執(zhí)行任務(wù)怯晕。同時蘋果官方文檔也有提到CFRunLoopTimerNSTimertoll-free bridged的,這就一位著兩者之間可以相互轉(zhuǎn)換缸棵。
關(guān)于NSTimer和RunLoop之間的關(guān)系舟茶,可以參照之前的文章iOS定時器-- NSTimer&GCD定時器

1.7堵第、CFRunLoopObserverRef--觀察者

CFRunLoopObserver是觀察者吧凉,監(jiān)測RunLoop的各種狀態(tài)的變化。如下是CFRunLoopObserver的源碼型诚。

struct __CFRunLoopObserver {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;
    //對應(yīng)的runLoop對象
    CFRunLoopRef _runLoop;
    // 當(dāng)前的觀察的runloop個數(shù)
    CFIndex _rlCount;
    //runloop的狀態(tài)
    CFOptionFlags _activities;        /* immutable */
    CFIndex _order;            /* immutable */
    //回調(diào)
    CFRunLoopObserverCallBack _callout;    /* immutable */
    //上下文
    CFRunLoopObserverContext _context;    /* immutable, except invalidation */
};

RunLoop的source事件源來監(jiān)測是否有需要執(zhí)行的任務(wù)客燕,而observer則是監(jiān)測RunLoop本身的各種狀態(tài)的變化鸳劳,在合適的時機(jī)拋出回調(diào)狰贯,執(zhí)行不同類型的任務(wù)。RunLoop用于觀察的狀態(tài)如下:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),//即將進(jìn)入runloop
    kCFRunLoopBeforeTimers = (1UL << 1),//即將處理timer事件
    kCFRunLoopBeforeSources = (1UL << 2),//即將處理source事件
    kCFRunLoopBeforeWaiting = (1UL << 5),//即將進(jìn)入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),//即將喚醒
    kCFRunLoopExit = (1UL << 7),//runloop退出
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

1.8赏廓、小結(jié)

  1. RunLoop是一種高級的循環(huán)機(jī)制涵紊,讓程序持續(xù)運(yùn)行,并處理程序中的各種事件幔摸,讓線程在需要做事的時候忙起來摸柄,不需要的話就讓線程休眠
  2. RunLoop和線程是綁定在一起的,每條線程都有唯一一個與之對應(yīng)的RunLoop對象
  3. 每個RunLoop對象都會包含有若干個mode既忆,每個mode包含有唯一一個name驱负,若干個sources0事件,若干個sources1事件患雇,若干個timer事件跃脊,若干個observer事件和若干portRunLoop總是在某種特定的mode下運(yùn)行的,這個特定的mode便是_currentMode苛吱。
image

2酪术、RunLoop底層實(shí)現(xiàn)原理

2.1、RunLoop啟動

RunLoop啟動有兩個方法可供調(diào)用翠储,分別是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);
}

從上面這兩個方法的實(shí)現(xiàn)可以看出援所,CFRunLoopRun方法啟動的RunLoop是運(yùn)行在kCFRunLoopDefaultMode模式下的庐舟,即以這種方式啟動的runloop是在默認(rèn)模式下運(yùn)行的。而CFRunLoopRunInMode方法則是需要指定運(yùn)行的mode住拭。從這里也可以看出來RunLoop雖然有很多的mode挪略,但是RunLoop的運(yùn)行只能是在一種mode下進(jìn)行耻涛。同時這兩個方法都調(diào)用了CFRunLoopRunSpecific方法,該方法就是具體啟動RunLoop的方法瘟檩,這個方法的第一個參數(shù)就是當(dāng)前的RunLoop抹缕,所以在分析CFRunLoopRunSpecific方法之前,先來看下是怎么獲取到RunLoop的墨辛。

2.2卓研、獲取RunLoop

蘋果開放給開發(fā)者連個方法獲取RunLoop對象任连,分別是CFRunLoopGetCurrentCFRunLoopGetMain砾医,它們分別代表著獲取當(dāng)前線程的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的實(shí)現(xiàn)源碼授账,從這段代碼可以看出該方法內(nèi)部調(diào)用了_CFRunLoopGet0方法镰矿,傳入的參數(shù)是當(dāng)前線程pthread_self(),由此可見CFRunLoopGetCurrent函數(shù)必須要在線程內(nèi)部調(diào)用才能獲取到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的實(shí)現(xiàn)源碼凿渊,從這段代碼可以看出該方法內(nèi)部調(diào)用了_CFRunLoopGet0方法梁只,傳入的參數(shù)是主線程pthread_main_thread_np(),由此可見CFRunLoopGetCurrent函數(shù)不管是在主線程中還是在子線程中都可以獲取到主線程的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);
        //創(chuàng)建一個字典
        CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
        //創(chuàng)建主線程的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);
    }
    // 根據(jù)線程從__CFRunLoops獲取RunLoop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    //如果在__CFRunLoops中沒有找到
    if (!loop) {
        //創(chuàng)建一個新的RunLoop
        CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
        loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        if (!loop) {
            //把新創(chuàng)建的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);
    }
    //如果傳入的線程就是當(dāng)前的線程
    if (pthread_equal(t, pthread_self())) {
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            //注冊一個回調(diào),當(dāng)當(dāng)前線程銷毀時銷毀對應(yīng)的RunLoop
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS - 1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;
}

上面這段關(guān)于CFRunLoopGet0函數(shù)方法的代碼并不復(fù)雜堵幽,我們可以從中得到如下幾個信息:

  1. RunLoop和線程是一一對應(yīng)的狗超,是以線程為key,RunLoop對象為value存放在一個全局字典中的朴下。
  2. 主線程的RunLoop會在初始化全局化字典時創(chuàng)建努咐。
  3. 子線程的RunLoop會在第一次獲取時創(chuàng)建。
  4. 當(dāng)線程銷毀時桐猬,對應(yīng)的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);
    //根據(jù)modeName找到本次運(yùn)行的mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    //如果沒有找到mode或者找到的mode中沒有注冊事件則退出免胃,不進(jìn)入循環(huán)
    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);
    //取上一次運(yùn)行的mode
    CFRunLoopModeRef previousMode = rl->_currentMode;
    rl->_currentMode = currentMode;
    int32_t result = kCFRunLoopRunFinished;
    //通知observer即將進(jìn)入RunLoop
    if (currentMode->_observerMask & kCFRunLoopEntry)
        __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    //通知observer已經(jīng)退出RunLoop
    if (currentMode->_observerMask & kCFRunLoopExit)
        __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

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

如上是CFRunLoopRunSpecific方法的實(shí)現(xiàn)代碼,這段代碼看上去復(fù)雜惫撰,其實(shí)很簡單羔沙。這個方法需要傳入四個參數(shù):

  • rl:當(dāng)前運(yùn)行的RunLoop對象。
  • modeName:指定RunLoop對象的mode的名稱厨钻。
  • seconds:RunLoop的超時時間
  • returnAfterSourceHandled:是否在處理完事件之后返回扼雏。

從上面的代碼我們可以獲取到如下幾點(diǎn)信息:

  1. RunLoop運(yùn)行必須要指定一個mode坚嗜,否則不會運(yùn)行RunLoop。
  2. 如果指定的mode沒有注冊時間任務(wù)诗充,RunLoop不會運(yùn)行苍蔬。
  3. 通知observer進(jìn)入runloop,調(diào)用__CFRunLoopRun方法處理任務(wù)蝴蜓,通知observer退出runloop碟绑。

2.4、__CFRunLoopRun

如下是__CFRunLoopRun方法的源碼(摘自iOS RunLoop詳解茎匠,小編懶得為代碼添加注釋????)格仲。

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    //獲取系統(tǒng)啟動后的CPU運(yùn)行時間,用于控制超時時間
    uint64_t startTSR = mach_absolute_time();
    
    //如果RunLoop或者mode是stop狀態(tài)诵冒,則直接return凯肋,不進(jìn)入循環(huán)
    if (__CFRunLoopIsStopped(rl)) {
        __CFRunLoopUnsetStopped(rl);
        return kCFRunLoopRunStopped;
    } else if (rlm->_stopped) {
        rlm->_stopped = false;
        return kCFRunLoopRunStopped;
    }
    
    //mach端口,在內(nèi)核中汽馋,消息在端口之間傳遞侮东。 初始為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端口賦值為主線程收發(fā)消息的端口
    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管理的定時器惭蟋,用于實(shí)現(xiàn)runloop超時機(jī)制
    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為超時時間苗桂,超時時執(zhí)行__CFRunLoopTimeout函數(shù)
    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;
    }
    
    //標(biāo)志位默認(rèn)為true
    Boolean didDispatchPortLastTime = true;
    //記錄最后runloop狀態(tài)药磺,用于return
    int32_t retVal = 0;
    do {
        //初始化一個存放內(nèi)核消息的緩沖池
        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
        //取所有需要監(jiān)聽的port
        __CFPortSet waitSet = rlm->_portSet;
        
        //設(shè)置RunLoop為可以被喚醒狀態(tài)
        __CFRunLoopUnsetIgnoreWakeUps(rl);
        
        //2.通知observer告组,即將觸發(fā)timer回調(diào),處理timer事件
        if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        //3.通知observer癌佩,即將觸發(fā)Source0回調(diào)
        if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
        
        //執(zhí)行加入當(dāng)前runloop的block
        __CFRunLoopDoBlocks(rl, rlm);
        
        //4.處理source0事件
        //有事件處理返回true木缝,沒有事件返回false
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        if (sourceHandledThisLoop) {
            //執(zhí)行加入當(dāng)前runloop的block
            __CFRunLoopDoBlocks(rl, rlm);
        }
        
        //如果沒有Sources0事件處理 并且 沒有超時,poll為false
        //如果有Sources0事件處理 或者 超時围辙,poll都為true
        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
        
        //第一次do..whil循環(huán)不會走該分支我碟,因為didDispatchPortLastTime初始化是true
        if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
            //從緩沖區(qū)讀取消息
            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即將進(jìn)入休眠
        if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        //設(shè)置RunLoop為休眠狀態(tài)
        __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
        //這里有個內(nèi)循環(huán)矫俺,用于接收等待端口的消息
        //進(jìn)入此循環(huán)后,線程進(jìn)入休眠掸冤,直到收到新消息才跳出該循環(huán)厘托,繼續(xù)執(zhí)行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的休眠狀態(tài)
        __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重新循環(huán)
            // 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執(zhí)行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) {
            //進(jìn)入run loop時傳入的參數(shù),處理完事件就返回
            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;
        }
        //除了上面這幾種情況饺藤,都繼續(xù)循環(huán)
    } while (0 == retVal);
    
    if (timeout_timer) {
        dispatch_source_cancel(timeout_timer);
        dispatch_release(timeout_timer);
    } else {
        free(timeout_context);
    }
    
    return retVal;
}

__CFRunLoopRun方法的源碼很長包斑,看上出寫了一堆亂七八糟的東西流礁,實(shí)際上該方法內(nèi)部就是一個 do-while 循環(huán),當(dāng)調(diào)用該方法時罗丰,線程就會一直留在這個循環(huán)里面神帅,直到超時或者手動被停止,該方法才會返回萌抵。在這里循環(huán)里面枕稀,線程在空閑的時候處于休眠狀態(tài),在有事件需要處理的時候谜嫉,處理事件萎坷。該方法是整個RunLoop運(yùn)行的核心方法。蘋果官方文檔對于RunLoop處理各類事件的流程有著詳細(xì)的描述沐兰。

  1. 通知觀察者RunLoop已經(jīng)啟動哆档。
  2. 通知觀察者定時器即將觸發(fā)。
  3. 通知觀察者任何不基于端口的輸入源都將觸發(fā)住闯。
  4. 觸發(fā)所有準(zhǔn)備觸發(fā)的非基于端口的輸入源瓜浸。
  5. 如果基于端口的輸入源已準(zhǔn)備好并等待啟動,立即處理事件比原;并進(jìn)入步驟9插佛。
  6. 通知觀察者線程進(jìn)入休眠狀態(tài)。
  7. 使線程進(jìn)入睡眠狀態(tài)量窘,直到發(fā)生以下事件之一:
    • 某一事件到達(dá)基于端口的源雇寇。
    • 定時器觸發(fā)。
    • RunLoop設(shè)置的時間已經(jīng)超時蚌铜。
    • RunLoop被喚醒锨侯。
  8. 通知觀察者線程即將被喚醒。
  9. 處理未處理的事件冬殃。
    • 如果用戶定義的定時器啟動囚痴,處理定時器事件并重啟RunLoop。進(jìn)入步驟2审葬。
    • 如果輸入源啟動深滚,傳遞相應(yīng)的消息。
    • 如果RunLoop被喚醒而且時間還沒超時涣觉,重啟RunLoop痴荐。進(jìn)入步驟2。
  10. 通知觀察者RunLoop結(jié)束旨枯。

整個流程如下圖所示:


image

2.5蹬昌、__CFRunLoopServiceMachPort

如果你仔細(xì)看過__CFRunLoopRun方法的代碼實(shí)現(xiàn),就會發(fā)現(xiàn)在其方法內(nèi)部有一個內(nèi)置的循環(huán)攀隔,這個循環(huán)會讓線程進(jìn)入休眠狀態(tài)皂贩,直到收到新消息才跳出該循環(huán)栖榨,繼續(xù)執(zhí)行RunLoop。這些消息是基于mach port來進(jìn)行進(jìn)程之間的通訊的明刷。

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;  //消息頭的標(biāo)志位
        msg->msgh_local_port = port;  //源(發(fā)出的消息)或者目標(biāo)(接收的消息)
        msg->msgh_remote_port = MACH_PORT_NULL; //目標(biāo)(發(fā)出的消息)或者源(接收的消息)
        msg->msgh_size = buffer_size;  //消息緩沖區(qū)大小婴栽,單位是字節(jié)
        msg->msgh_id = 0;  //唯一id
       
        if (TIMEOUT_INFINITY == timeout) { CFRUNLOOP_SLEEP(); } else { CFRUNLOOP_POLL(); }
        
        //通過mach_msg發(fā)送或者接收的消息都是指針,
        //如果直接發(fā)送或者接收消息體辈末,會頻繁進(jìn)行內(nèi)存復(fù)制愚争,損耗性能
        //所以XNU使用了單一內(nèi)核的方式來解決該問題,所有內(nèi)核組件都共享同一個地址空間挤聘,因此傳遞消息時候只需要傳遞消息的指針
        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);
        
        //接收/發(fā)送消息成功轰枝,給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
        //此時釋放緩沖區(qū)组去,把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
        //如果接收緩沖區(qū)太小鞍陨,則將過大的消息放在隊列中,并且出錯返回MACH_RCV_TOO_LARGE从隆,
        //這種情況下诚撵,只返回消息頭,調(diào)用者可以分配更多的內(nèi)存
        if (MACH_RCV_TOO_LARGE != ret) break;
        //此處給buffer分配更大內(nèi)存
        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的源碼键闺,該方法接收指定內(nèi)核端口的消息寿烟,并將消息緩存在緩存區(qū),供外界獲取辛燥。該方法的核心是mach_msg方法筛武,該方法實(shí)現(xiàn)消息的發(fā)送個接收。RunLoop調(diào)用這個函數(shù)去接收消息购桑,如果沒有接收到port的消息畅铭,內(nèi)核會將線程置于等待狀態(tài)。

2.6勃蜘、RunLoop事件處理

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

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

這些方法的實(shí)現(xiàn)我們不必關(guān)系眶俩,但是這些方法在處理事件后如何回調(diào)給上層莹汤,才是我們需要關(guān)心的。比如說__CFRunLoopDoSources0處理的是系統(tǒng)的事件颠印,那么觸發(fā)一個UIButton的點(diǎn)擊事件后纲岭,查看函數(shù)調(diào)用棧應(yīng)該可以知道回到給上層是如何進(jìn)行的抹竹。

image

如上圖所示UIButton的點(diǎn)擊事件的函數(shù)調(diào)用棧,我們可以清楚的看到__CFRunLoopDoSources0方法的調(diào)用止潮,然后調(diào)用__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__回到UIKit層窃判。

關(guān)于上述方法回調(diào)上層的方法對應(yīng)如下圖所示:


image

2.7、小結(jié)

  1. RunLoop的運(yùn)行必定要指定一種mode喇闸,并且該mode必須注冊任務(wù)事件袄琳。
  2. RunLoop是在默認(rèn)mode下運(yùn)行的,當(dāng)然也可以指定一種mode運(yùn)行燃乍,但是只能在一種mode下運(yùn)行唆樊。
  3. RunLoop內(nèi)部實(shí)際上是維護(hù)了一個do-while循環(huán),線程就會一直留在這個循環(huán)里面刻蟹,直到超時或者手動被停止窗轩。
  4. RunLoop 的核心就是一個 mach_msg() ,RunLoop 調(diào)用這個函數(shù)去接收消息座咆,如果沒有別人發(fā)送 port 消息過來痢艺,內(nèi)核會將線程置于等待狀態(tài),否則線程處理事件介陶。

3堤舒、RunLoop應(yīng)用

3.1、NSTimer

用過NSTimer來做定時器開發(fā)的人都知道哺呜,NSTimer對象需要添加到RunLoop中才能正確執(zhí)行舌缤。在前面的內(nèi)容也講到了CFRunLoopTimerNSTimertoll-free bridged的。一個NSTimer注冊到 RunLoop 后某残,RunLoop會為其重復(fù)的時間點(diǎn)注冊好事件国撵。尤其是在一個滾動視圖中使用NSTimer時,由于其mode的變化導(dǎo)致NSTimer停止工作玻墅,解決這個問題的關(guān)鍵就是講NSTimer注冊到RunLoopNSRunLoopCommonModes下介牙。NSTimer更多的內(nèi)容請移步iOS定時器-- NSTimer&GCD定時器

GCD則不同澳厢,GCD的線程管理是通過系統(tǒng)來直接管理的环础。GCD Timer是通過dispatch portRunLoop發(fā)送消息,來使RunLoop執(zhí)行相應(yīng)的block剩拢,如果所在線程沒有RunLoop线得,那么GCD 會臨時創(chuàng)建一個線程去執(zhí)行block,執(zhí)行完之后再銷毀掉徐伐,因此GCDTimer是不依賴RunLoop的贯钩。

3.2、AutoReleasepool

一般很少會將自動釋放池和RunLoop聯(lián)系起來,但是如果打印[NSRunLoop currentRunLoop]結(jié)果中會發(fā)現(xiàn)和自動釋放池相關(guān)的回調(diào)角雷。

<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,其中有兩個是跟自動釋放池相關(guān)的谓罗,其回調(diào)都是_wrapRunLoopWithAutoreleasePoolHandler()粱哼。\

  • 第一個observer監(jiān)聽的是activities=0x1(kCFRunLoopEntry),也就是在即將進(jìn)入loop時檩咱,其回調(diào)會調(diào)用_objc_autoreleasePoolPush() 創(chuàng)建自動釋放池揭措;
  • 第二個observer監(jiān)聽的是activities = 0xa0(kCFRunLoopBeforeWaiting | kCFRunLoopExit)
    即監(jiān)聽的是準(zhǔn)備進(jìn)入睡眠和即將退出loop兩個事件刻蚯。在準(zhǔn)備進(jìn)入睡眠之前绊含,因為睡眠可能時間很長,所以為了不占用資源先調(diào)用_objc_autoreleasePoolPop()釋放舊的釋放池炊汹,并調(diào)用_objc_autoreleasePoolPush() 創(chuàng)建新建一個新的躬充,用來裝載被喚醒后要處理的事件對象;在最后即將退出loop時則會 _objc_autoreleasePoolPop()釋放池子讨便。

關(guān)于自動釋放池更多的內(nèi)容請移步iOS內(nèi)存管理二:自動釋放池autoreleasepool

3.3充甚、卡頓檢測

我們可以通過RunLoop的不同狀態(tài)來做頁面刷新的卡頓檢測。

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

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

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

- (void)startMonitor{
    // 創(chuàng)建信號
    _semaphore = dispatch_semaphore_create(0);
    // 在子線程監(jiān)控時長
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (YES)
        {
            //超時時間是 1 秒霸褒,沒有等到信號量伴找,st 就不等于 0, RunLoop 所有的任務(wù)
            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;
        }
    });
}

如上面的代碼所示就是一段簡單的監(jiān)測頁面卡頓的代碼废菱,其原理就是根據(jù)RunLoop進(jìn)入kCFRunLoopBeforeSources狀態(tài)處理source事件到kCFRunLoopAfterWaiting狀態(tài)變化之間的時間間隔來做判斷依據(jù)技矮。當(dāng)然這只是一個簡單的demo,但是RunLoop各種狀態(tài)的變化為很多優(yōu)秀的卡頓檢測的三方庫提供了理論基礎(chǔ)殊轴。

3.4衰倦、常駐線程

有的時候我們需要創(chuàng)建一個線程在后臺一直做一些任務(wù),但是常規(guī)的線程在任務(wù)完成后就會立即銷毀旁理,因此我們需要一個常駐線程來讓線程一直都存在樊零。

- (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啟動前必須要設(shè)置一個mode淹接,并且為mode至少設(shè)置一個Source/Timer/Observer,在這里是添加了一個port叛溢,雖然消息可以通過port發(fā)送到RunLoop內(nèi),但是這里并沒有發(fā)送任何的消息劲适,所以這樣便可以保持RunLoop不退出,s實(shí)現(xiàn)線程常駐楷掉。

參考資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子烹植,更是在濱河造成了極大的恐慌斑鸦,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件草雕,死亡現(xiàn)場離奇詭異巷屿,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)墩虹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進(jìn)店門嘱巾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人诫钓,你說我怎么就攤上這事旬昭。” “怎么了菌湃?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵问拘,是天一觀的道長。 經(jīng)常有香客問我惧所,道長骤坐,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任下愈,我火速辦了婚禮纽绍,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘驰唬。我一直安慰自己顶岸,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布叫编。 她就那樣靜靜地躺著辖佣,像睡著了一般。 火紅的嫁衣襯著肌膚如雪搓逾。 梳的紋絲不亂的頭發(fā)上卷谈,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天,我揣著相機(jī)與錄音霞篡,去河邊找鬼世蔗。 笑死,一個胖子當(dāng)著我的面吹牛朗兵,可吹牛的內(nèi)容都是我干的污淋。 我是一名探鬼主播,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼余掖,長吁一口氣:“原來是場噩夢啊……” “哼寸爆!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤赁豆,失蹤者是張志新(化名)和其女友劉穎仅醇,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體魔种,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡析二,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了节预。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片叶摄。...
    茶點(diǎn)故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖心铃,靈堂內(nèi)的尸體忽然破棺而出准谚,到底是詐尸還是另有隱情,我是刑警寧澤去扣,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布柱衔,位于F島的核電站,受9級特大地震影響愉棱,放射性物質(zhì)發(fā)生泄漏唆铐。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一奔滑、第九天 我趴在偏房一處隱蔽的房頂上張望艾岂。 院中可真熱鬧,春花似錦朋其、人聲如沸王浴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽氓辣。三九已至,卻和暖如春袱蚓,著一層夾襖步出監(jiān)牢的瞬間钞啸,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工喇潘, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留体斩,地道東北人。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓颖低,卻偏偏與公主長得像絮吵,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子忱屑,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評論 2 353

推薦閱讀更多精彩內(nèi)容

  • 一源武、概述 一般來說扼褪,一個線程只能執(zhí)行一個任務(wù)想幻,執(zhí)行完就會退出粱栖,如果我們需要一種機(jī)制,讓線程能隨時處理時間但并不退出...
    一直在路上66閱讀 287評論 0 0
  • ios RunLoop詳解 一脏毯、概述 一般來說,一個線程只能執(zhí)行一個任務(wù)闹究,執(zhí)行完就會退出。如果我們需要一種機(jī)制,讓...
    721e472431a4閱讀 795評論 0 1
  • RunLoop簡介 從字面意思來看是運(yùn)行循環(huán)食店,在程序運(yùn)行過程中循環(huán)做一些事情渣淤,如果沒有Runloop程序執(zhí)行完畢就...
    一直很安靜_25ae閱讀 404評論 0 0
  • 二、runloop應(yīng)用 2.1 NSTimer 前面一直提到Timer Source作為事件源吉嫩,事實(shí)上它的上層對應(yīng)...
    leonardni閱讀 3,044評論 0 3
  • 一.RunLoop介紹 1.概念 RunLoop是一個運(yùn)行循環(huán)价认,正是因為RunLoop,IOS才可以保持程序的持續(xù)...
    一片姜汁閱讀 383評論 0 0