Objective-C基礎之八(深刻理解RunLoop)

什麼是RunLoop?

RunLoop實際上是一個事件處理循環,被用做工做調度而且協調傳入事件的接收。通常狀況下,單條線程一次只能執行一個任務,執行完成以後線程就會退出,若是咱們但願線程可以隨時的處理事件而且不會退出,那麼就在線程中開啓一個RunLoop,RunLoop其實就是一個運行循環,它的主要目的是可以讓線程在有工做的時候保持忙碌,在沒有工做的時候進入休眠,這樣作的好處就是讓線程進入休眠以後避免資源佔用。html

RunLoop的結構以下安全

RunLoop在循環過程當中處理定時器事件、performSelector事件、自定義源以及端口事件等等。bash

RunLoop的整個運行邏輯其實能夠概括爲如下代碼markdown

function loop() {
    initialize();
    do {
        //休眠中等待消息
        var message = get_next_message();
        //處理消息
        process_message(message);
    } while (message != quit);
}
複製代碼

因而可知,其實RunLoop就是一個對象,它管理了須要進行處理的事件和消息,而且爲線程提供了一個如上的入口函數,當線程執行了這個入口函數以後,就會進入一個「接收消息->休眠等待->處理消息」的循環之中,一直到循環結束。app

Cocoa和Core Foundation框架都提供了運行循環對象來幫助咱們配置和管理線程的運行循環:NSRunLoop和CFRunLoopRef框架

  • CFRunLoopRef是Core Foundation框架提供的,是純C函數的API,全部相關的API都是線程安全的。源碼能夠參考CF源碼下載
  • NSRunLoop則是對CFRunLoopRef進行了封裝,是面向對象的,它不是線程安全的。

RunLoop的主要做用能夠總結爲如下幾點:異步

  • 可以保持程序持續運行(主線程RunLoop默認開啓)
  • 處理App中的各類事件(觸摸事件、定時器)
  • 節省CPU資源,提升程序性能(有消息就處理消息,無消息就進入休眠狀態,不佔用資源)

RunLoop和線程的聯繫

運行循環其實不是徹底自動的,當咱們建立一個線程的時候須要在適當的時間啓動運行循環而且響應傳入的事件,上文中說到Cocoa和Core Foundation框架都提供了運行循環對象來幫助咱們配置和管理線程的運行循環,所以咱們不須要顯式的建立運行循環對象,程序中的每一個線程,包括應用程序的主線程都有一個關聯的RunLoop對象。可是,只有子線程須要顯式的開啓它們的運行循環。而主線程做爲應用程序啓動過程的一部分,應用程序框架會在主線程上自動設置而且運行RunLoop。函數

蘋果的API爲咱們提供了兩種方法獲取RunLoop對象oop

  • 在Cocoa框架中,咱們使用[NSRunLoop mainRunLoop]獲取主線程的RunLoop對象,使用[NSRunLoop currentRunLoop]獲取當前線程的RunLoop對象。
  • 在Core Foundation框架中,咱們使用CFRunLoopGetMain()獲取主線程的RunLoop對象,使用CFRunLoopGetCurrent()獲取當前線程的RunLoop對象。

閱讀CF源碼,咱們能夠獲得獲取RunLoop函數的內部邏輯以下性能

//獲取線程對應的RunLoop,若是t=0表示獲取主線程的RunLoop
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    //若是當前線程爲nil
    if (pthread_equal(t, kNilPthreadT)) {
        //獲取主線程
        t = pthread_main_thread_np();
    }
    __CFLock(&loopsLock);
    if (!__CFRunLoops) {
        __CFUnlock(&loopsLock);
        //第一次進入時,全局字典__CFRunLoops爲nil,因此須要初始化全局dic
        CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
        //首先爲主線程建立一個RunLoop
        CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
        //將主線程RunLoop存入全局字典中
        CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
	if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
	    CFRelease(dict);
	}
	CFRelease(mainLoop);
        __CFLock(&loopsLock);
    }
    //根據線程t到全局字典__CFRunLoops中獲取對應的RunLoop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    if (!loop) {
        //若是不存在對應RunLoop,則爲當前線程t新建一個RunLoop
        CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
        loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        if (!loop) {
            //而且將新建的RunLoop保存到全局字典中去
            CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
            loop = newLoop;
        }
        __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;
}
複製代碼

因而可知線程和RunLoop的關係以下

  • 線程和RunLoop之間一一對應,每條線程都有惟一一個與之對應的RunLoop對象
  • RunLoop保存在一個全局的字典裏,線程做爲Key,RunLoop對象做爲value
  • 線程剛建立時並無RunLoop對象,RunLoop對象會在第一次獲取它的時候進行建立
  • RunLoop會在線程結束的時候進行銷燬
  • 主線程的RunLoop自動建立並啓動,子線程默認沒有開啓RunLoop,而且只能在線程內部獲取其RunLoop,主線程除外。

RunLoop底層結構

在Core Foundation框架中提供了關於RunLoop的5個類

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

它們之間的關係以下:

在一個RunLoop對象當中包含了多個Mode,而每一個Mode又包含了若干個Source0/Source1/Timer/Observer,RunLoop啓動的時候只能指定其中的一個Mode,這個Mode被稱爲CurrentMode。若是須要切換Mode,只能先退出當前Loop,而後從新指定Mode再進入Loop。這樣作的主要目的就是爲了分割不一樣組的Source0/Source1/Timer/Observer,讓它們互不影響。

若是Mode中沒有任何的Source0/Source1/Timer/Observer,那麼RunLoop會當即退出。這裏的Source0/Source1/Timer/Observer統稱爲一個Mode item。

CFRunLoopRef

CFRunLoopRef其實就是Core Foundation框架提供的RunLoop對象。

CFRunLoopModeRef

CFRunLoopModeRef其實就是就是多個Source0/Source1/Timer/Observer的集合。每次運行RunLoop循環時,都要指定特定的模式,在RunLoop循環過程中,只監視與該模式相關聯的源,而且容許它們進行事件交付。相似的,也只有關聯了該模式的觀察者才能監聽到RunLoop的狀態變化。CFRunLoopModeRef的大體結構以下

struct __CFRunLoopMode {
    CFStringRef _name;              //Mode名稱
    Boolean _stopped;
    char _padding[3];   
    CFMutableSetRef _sources0;      //Source0
    CFMutableSetRef _sources1;      //Source1
    CFMutableArrayRef _observers;   //觀察者集合
    CFMutableArrayRef _timers;      //定時器集合
    ......
}

struct __CFRunLoop {
    __CFPort _wakeUpPort;			     // used for CFRunLoopWakeUp 
    Boolean _unused;
    volatile _per_run_data *_perRunData; // reset for runs of the run loop
    CFMutableSetRef _commonModes;        // 全部標記爲Common的Mode的name集合       
    CFMutableSetRef _commonModeItems;    // 全部被標記爲Common的Source/Observer/Timer
    CFRunLoopModeRef _currentMode;       // 當前RunLoop指定的Mode
    CFMutableSetRef _modes;              // 全部Mode集合
    ......
};
複製代碼

Mode中主要包含如下幾種元素:

  • Mode的名稱
  • Source0的集合
  • Source1的集合
  • 觀察者集合observers
  • 定時器的集合timers

蘋果提供了兩種公開的Mode,kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和UITrackingRunLoopMode

  • NSDefaultRunLoopMode是RunLoop默認的Mode,主線程的RunLoop通常狀況下都會處於這種狀態。
  • UITrackingRunLoopMode是用於追蹤UIScrollView滑動的一種Mode,當程序監聽到ScrollView滑動時,RunLoop會切換到此Mode下

而在RunLoop對象中,集合_modes包含了全部RunLoop支持的Mode。同時,RunLoop還支持一種「CommonModes」的概念,每一個Mode都能將其標記爲「Common」屬性(具體是將Mode的name添加到RunLoop的_commonModes集合當中),主線程的RunLoop兩個預置的Mode:kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和UITrackingRunLoopMode都已經被標記爲了「Common」。

RunLoop中的_commonModeItems集合就是用來存放被標記爲common的Source/Observer/Timer,當RunLoop狀態發生變化時,會將_commonModeItems中的全部的Source/Observer/Timer同步到具備common標識的全部Mode中。

以定時器舉例,咱們在主線程中添加一個定時器,而且添加到NSDefaultRunLoopMode當中,定時器會正常回調。此時若是界面上存在ScrollView,而且滑動ScrollView,RunLoop就會切換到UITrackingRunLoopMode模式下,此時,因爲定時器只存在於NSDefaultRunLoopMode模式中,一旦切換到UITrackingRunLoopMode模式,定時器便會中止,等待RunLoop從新切換到DefaultMode時恢復運行。若是想要定時器在兩種模式下都可以正常運行,能夠將定時器同時添加到兩種Mode中,還有一種方式,就是將定時器標記爲「common」,其實也就是將定時器加入到RunLoop對象的_commonModeItems中去。此時RunLoop會自動將定時器同步到具備Common標記的Mode中去。代碼以下:

NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
    NSLog(@"定時器任務");
}];
//方法一
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
//方法二
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

複製代碼

除了使用蘋果公開的Mode外,咱們還能夠建立自定義Mode,具體接口以下

//指定RunLoop建立Mode,主要是經過mode name來建立,若是RunLoop內部根據mode name沒有發現對應的mode,RunLoop則會自動建立CFRunLoopModeRef
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);
複製代碼

同時,Mode中也提供了對Mode Item的操做函數,以下

CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
複製代碼

CFRunLoopSourceRef

CFRunLoopSourceRef又稱之爲輸入源,它是事件產生的地方。輸入源是將事件異步傳遞到線程中。事件的源則取決於輸入源的類型,而輸入源通常分爲兩類:第一類是自定義輸入源,監視自定義事件源,又稱爲Source0。第二類是基於端口的輸入源,監視應用程序的Mach端口,又稱爲Source1

  • Source0 只包含一個回調,它不能主動觸發事件,使用Source0時,須要先調用CFRunLoopSourceSignal(source)將Source標記爲待處理,而後手動調用CFRunLoopWakeUp(runloop)來喚醒RunLoop,讓它來處理這個事件。簡單來講,Source0其實就是負責App內部事件的處理(例如UITouch事件等等)
  • Source1包含了一個mach_port和一個回調指針,被用於經過內核與其它線程進行通訊。Source1可以主動喚醒RunLoop的線程。簡單來講,Source1其實就是用來接收系統發出的事件(例如手機的觸摸、搖晃或者鎖屏等等)

舉一個簡單的例子:當App在前臺靜止時,若是咱們點擊App的頁面,此時咱們首先接觸的是手機屏幕,此時,觸摸屏幕的事件會被包裝成Event傳遞給source1,而後source1主動喚醒RunLoop,以後將事件傳遞給Source0來進行處理。

CFRunLoopTimerRef

CFRunLoopTimerRef是定時器源,它會在未來某個預設的時間點將事件同步發送到線程。定時器是線程通知本身作某件事的一種方式。定時器生成基於時間的通知,可是它並非實時的。與輸入源同樣,計時器和運行循環的特定mode相關聯。

  • 若是計時器沒有處於運行循環當前監聽的模式中,那麼定時器不會被觸發,直到運行循環切換到定時器支持的運行模式。
  • 相似的,若是定時器在運行循環正在處理程序任務時觸發,那麼定時器會等待下一次運行循環時觸發。
  • 若是線程沒有開啓RunLoop,定時器也不會觸發。

咱們能夠配置定時器來生成一次或者屢次事件,重複定時器會根據預約的觸發時間(而不是實際的觸發時間)自動從新調度本身。例如一個定時器在一個特定的時間觸發,而且在以後每5s觸發一次,那麼計劃的觸發時間將始終落在最初的5s間隔上。若是此時RunLoop正在處理一個耗時的任務,那麼定時器的觸發時間會被延時。假設RunLoop執行的耗時任務爲12s,那麼在RunLoop執行完完耗時任務以後,定時器會當即觸發一次,而後定時器會從新安排下一次預約的觸發時間。也就是說在耗時的12s內,只會觸發一次定時器。

CFRunLoopObserverRef

CFRunLoopObserverRef是觀察者,每個Observer都包含一個回調,當RunLoop狀態發生變化時,觀察者就可以經過回調接收到變化狀態。RunLoop有如下幾種狀態能被觀測

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),           //即將進入loop
    kCFRunLoopBeforeTimers = (1UL << 1),    //即將處理Timer
    kCFRunLoopBeforeSources = (1UL << 2),   //即將處理Source
    kCFRunLoopBeforeWaiting = (1UL << 5),   //即將進入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),    //從休眠中喚醒
    kCFRunLoopExit = (1UL << 7),            //即將退出loop
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};
複製代碼

在代碼中,咱們可使用如下方式來監聽RunLoop的狀態變化

CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry:{
                NSLog(@"即將進入loop");
                break;
            }
            case kCFRunLoopBeforeTimers:{
                NSLog(@"即將處理Timer");
                break;
            }
            case kCFRunLoopBeforeSources:{
                NSLog(@"即將處理Source");
                break;
            }
            case kCFRunLoopBeforeWaiting:{
                NSLog(@"即將進入休眠");
                break;
            }
            case kCFRunLoopAfterWaiting:{
                NSLog(@"從休眠中喚醒");
                break;
            }
            case kCFRunLoopExit:{
                NSLog(@"即將退出loop");
                break;
            }
            default:
                break;
        }
    });
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes);
    CFRelease(observer);
複製代碼

RunLoop運行邏輯

RunLoop運行邏輯流程圖以下:

  1. 通知Observers:即將進入loop
  2. 通知Observers:即將處理Timers
  3. 通知Observers:即將處理Source0
  4. 開始處理Source0
  5. 若是此時有Source1,則跳到第9步
  6. 通知Observers:線程即將休眠
  7. 讓線程休眠,直到如下時機喚醒
    • 接收到Source1
    • 啓動Timer
    • 爲運行循環設置超時值過時
    • 被外部手動喚醒
  8. 通知Observers:線程剛剛被喚醒
  9. 接收到消息,開始處理掛起事件
    • 若是用戶自定義的Timer觸發,則處理Timer事件並從新啓動循環,跳轉到步驟2
    • 若是此時觸發了輸入源,則傳遞事件
    • 若是運行循環被手動喚醒,可是還沒有超時,則從新啓動循環,跳轉到步驟2
  10. 通知Observers:退出loop

此處是對照官方文檔的流程,其中忽略了對block的處理

RunLoop源碼

想要查看RunLoop源碼,首先須要知道RunLoop的入口函數,方法很簡單,新建項目,在項目啓動後在任意處斷點,而後經過LLDB指令bt能夠獲得調用棧以下:

其中CFRunLoopRunSpecific函數就是RunLoop的入口函數,以下(此處只保留部分主要代碼):

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */

    //第一步、通知Observers:即將進入loop
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    //內部函數,進入loop
	result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    //第十步、通知Observers:退出loop
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    
    return result;
}
複製代碼

loop的真正核心就是__CFRunLoopRun函數,源碼以下:

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    
    Boolean didDispatchPortLastTime = true;
    int32_t retVal = 0;
    do {
        //第二步、通知Observers:即將處理Timers
         __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        //第三步、通知Observers:即將處理Source0(非port)
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
        //執行被加入的block
        __CFRunLoopDoBlocks(rl, rlm);
        
        //第四步、處理Source0,觸發Source0回調
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        if (sourceHandledThisLoop) {
            //再次執行執行被加入的block
            __CFRunLoopDoBlocks(rl, rlm);
        }

        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
        //第五步、判斷是否有Source1須要進行處理,若是有,跳轉到handle_msg標記處執行
        if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
            msg = (mach_msg_header_t *)msg_buffer;
            if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
                goto handle_msg;
            }
        }
        didDispatchPortLastTime = false;
        //第六步、通知Observers,RunLoop所在線程即將進入休眠
        if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
            
        //第七步、調用內核函數mach_msg,讓線程進入休眠,直到被如下事件喚醒
        //一、接收到Source1事件
        //二、啓動Timer
        //三、爲運行循環設置超時值過時
        //四、被外部手動喚醒
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
    
        //第八步、通知Observers:RunLoop線程剛被喚醒
        if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

    //第九步、接收到消息開始進行處理
    handle_msg:;
        if (被Timer喚醒) {
            //9-一、若是用戶自定義的計時器觸發,則處理計時器事件並從新啓動循環
            __CFRunLoopDoTimers(rl, rlm, mach_absolute_time()
        }
        else if (被GCD喚醒) {
            //9-二、若是GCD子線程中有回到主線程的操做,那麼喚醒線程,執行主線程的block
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
        } else {
            //9-三、若是有基於port的Source1事件傳入,則處理Source1事件
            CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
            if (rls) {
                mach_msg_header_t *reply = NULL;
                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);                }
            }
        }
        //再次執行被加入的block
        __CFRunLoopDoBlocks(rl, rlm);
            
        if (sourceHandledThisLoop && stopAfterHandle) {
            //若是當前事件處理完成就直接返回
            retVal = kCFRunLoopRunHandledSource;
        } else if (timeout_context->termTSR < mach_absolute_time()) {
            //RunLoop到超時時間了
            retVal = kCFRunLoopRunTimedOut;
        } else if (__CFRunLoopIsStopped(rl)) {
            //RunLoop被強制中止了
            retVal = kCFRunLoopRunStopped;
        } else if (rlm->_stopped) {
            rlm->_stopped = false;
            retVal = kCFRunLoopRunStopped;
        } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
            //若是RunLoop當前指定的Mode中Source/Timer/Observer都爲空
            retVal = kCFRunLoopRunFinished;
        }
        //若是RunLoop沒有超時,沒有被中止,指定的Mode中存在ModeItem,則一直運行RunLoop
        } while (0 == retVal);

        return retVal;
}
複製代碼

RunLoop回調

RunLoop在進行回調時,都會調用一個特別長的函數,例如調用__CFRunLoopDoObservers通知Observers即將進入RunLoop時,內部會調用CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION函數。如下將源碼中對應的函數轉換成了實際調用的函數,以下

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */

    //第一步、通知Observers:即將進入loop
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
    //內部函數,進入loop
	result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    //第十步、通知Observers:退出loop
     __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
    
    return result;
}

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {

    Boolean didDispatchPortLastTime = true;
    int32_t retVal = 0;
    do {
        //第二步、通知Observers:即將處理Timers
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
        //第三步、通知Observers:即將處理Source0(非port)
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
        //執行被加入的block
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
        
        //第四步、處理Source0,觸發Source0回調
        Boolean sourceHandledThisLoop = __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
        if (sourceHandledThisLoop) {
            //再次執行執行被加入的block
            __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
        }

        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
        //第五步、判斷是否有Source1須要進行處理,若是有,跳轉到handle_msg標記處執行
        if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
            msg = (mach_msg_header_t *)msg_buffer;
            if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
                goto handle_msg;
            }
        }
        didDispatchPortLastTime = false;
        //第六步、通知Observers,RunLoop所在線程即將進入休眠
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
            
        //第七步、調用內核函數mach_msg,讓線程進入休眠,直到被如下事件喚醒
        //一、接收到Source1事件
        //二、啓動Timer
        //三、爲運行循環設置超時值過時
        //四、被外部手動喚醒
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
    
        //第八步、通知Observers:RunLoop線程剛被喚醒
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);

    //第九步、接收到消息開始進行處理
    handle_msg:;
        if (被Timer喚醒) {
            //9-一、若是用戶自定義的計時器觸發,則處理計時器事件並從新啓動循環
            __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
        }
        else if (被GCD喚醒) {
            //9-二、若是GCD子線程中有回到主線程的操做,那麼喚醒線程,執行主線程的block
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
        } else {
            //9-三、若是有基於port的Source1事件傳入,則處理Source1事件
            __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
        }
        //再次執行被加入的block
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
            
        //若是RunLoop沒有超時,沒有被中止,指定的Mode中存在ModeItem,則一直運行RunLoop
        } while (0 == retVal);
        
        return retVal;
}
複製代碼

具體流程圖總結以下:

流程中穿插着對block的處理,其中的block能夠經過CFRunLoopPerformBlock函數來添加。

RunLoop應用

線程保活

RunLoop一個很重要的做用就是能用來控制線程的生命週期,也就是線程包活。主線程的RunLoop默認開啓,所以主線程一直處於活躍狀態,可是子線程默認沒有開啓RunLoop,因此子線程執行完任務以後就會被銷燬。早期AFNetworking 2.x版本就使用了RunLoop來保活線程。具體實現以下:

XLThread.h源碼以下

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

typedef void(^XLThreadTask)(void);

@interface XLThread : NSObject

/** 開始任務 */
- (void)executeTask:(XLThreadTask)task;

/** 結束線程 */
- (void)stop;

@end

NS_ASSUME_NONNULL_END
複製代碼

XLThread.m以下

#import "XLThread.h"
#import <objc/runtime.h>

@interface XLThread ()

@property(nonatomic, strong)NSThread *innerThread;

@end

@implementation XLThread

#pragma mark - Public
- (instancetype)init
{
    self = [super init];
    if (self) {
        
        self.innerThread = [[NSThread alloc] initWithTarget:self selector:@selector(__initThread) object:nil];
        [self.innerThread start];
    }
    return self;
}

- (void)executeTask:(XLThreadTask)task{
    if (!self.innerThread || !task) {
        return;
    }
    [self performSelector:@selector(__executeTask:) onThread:self.innerThread withObject:task waitUntilDone:NO];
}


- (void)stop
{
    if (!self.innerThread) return;
    
    [self performSelector:@selector(__stop) onThread:self.innerThread withObject:nil waitUntilDone:YES];
}

- (void)dealloc
{
    SBPLog(@"%s", __func__);
    
    [self stop];
}

#pragma mark - Private
- (void)__stop{
    CFRunLoopStop(CFRunLoopGetCurrent());
    self.innerThread = nil;
}

- (void)__executeTask:(XLThreadTask)task{
    task();
}

- (void)__initThread{
    //建立上下文
    CFRunLoopSourceContext context = {0};
    //建立source
    CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
    //向runloop中添加source
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
    //銷燬source
    CFRelease(source);
    //啓動runLoop
    CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);
}

@end
複製代碼

AutoreleasePool

在iOS中,AutoreleasePool其實也是基於RunLoop來實現的,App啓動時會在主線程的RunLoop中註冊兩個Observer。

  • 第一個Observer用來監聽RunLoop的Entry事件,在監聽到Entry事件時會調用_objc_autoreleasePoolPush()來建立自動釋放池。
  • 第二個Observer監聽兩種事件
    • 當RunLoop準備進入休眠(處於BeforeWaiting狀態)時,會調用_objc_autoreleasePoolPop()來釋放舊的池,而後調用_objc_autoreleasePoolPush()來建立新的自動釋放池
    • 當退出RunLoop(處於Exit狀態)時,會調用_objc_autoreleasePoolPop()來釋放AutoreleasePool

關於AutoreleasePool的具體實現會在後面的章節詳細介紹。

NSTimer

前面說到的CFRunLoopTimerRef其實就是NSTimer,NSTimer的觸發必須基於RunLoop,而且RunLoop須要處於開啓狀態。一般咱們會在主線程中使用定時器,由於主線程的RunLoop默認開啓,若是想要在子線程中使用定時器,就須要手動開啓子線程RunLoop。

當咱們建立NSTimer,而後將其註冊到RunLoop後,RunLoop會根據預設的觸發時間在重複的時間點註冊好事件,例如設置定時器在5:10分開始觸發,而且每隔5m觸發一次,定時器的觸發時間就是固定的5:十、5:1五、5:20、5:25等等,可是RunLoop爲了節省資源,並不會在很是準確的時間點觸發定時器。若是在觸發定時器以前RunLoop執行了一個很長的任務,以致於錯過了預設的時間點,那麼在長時間任務執行完成以後會當即觸發一次定時器,而後等到下一個預設的時間點再次觸發。

假設原定於5:10分觸發定時器,可是RunLoop因爲執行耗時任務到5:22,那麼此時,在耗時任務執行完成以後會當即觸發一次定時器任務,而後等待到5:25時再次觸發定時器。

PerformSelector

上文提到,RunLoop中輸入源分爲兩種,一種是基於port的輸入源,還有一種是自定義輸入源。

Cocoa框架爲咱們提供了一種自定義源(Perform Selector Source),可讓咱們在任何線程上執行Selector,而且在執行完Selector以後,該源會自動從RunLoop中移除。當咱們使用performSelector在另外一個線程中執行Selector時,必需要保證目標線程中有開啓RunLoop,所以就須要咱們顯式的啓動目標線程的RunLoop。

如下就是在NSObject中聲明的performSelector方法

方法 描述
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
在應用程序主線程的RunLoop的下一次循環週期內執行指定的Selector。
而且該方法能夠阻塞當前線程,直到執行Selector爲止。
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
在任何線程上指向指定的Selector。
而且該方法能夠阻塞當前線程,直到執行Selector爲止。
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
在下一個運行循環週期中以及可選的延遲時間以後,在當前線程上執行指定的selector。
由於是等到下一個運行循環週期執行selector,因此這些方法提供了當前執行代碼的最小自動延遲。
多個排隊的選擇器按照排隊的順序依次執行
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:
取消發送到當前線程的消息

舉一個簡單的例子,在touchesBegan:方法中添加如下示例代碼

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"任務1");
    [self performSelector:@selector(test) withObject:nil afterDelay:.0];
    NSLog(@"任務3");
}

- (void)test{
    NSLog(@"任務2");
}
複製代碼

當點擊頁面時,執行順序依次是任務一、任務3和任務2。緣由很簡單,由於performSelector:withObject:afterDelay方法本質實際上是建立一個定時器NSTimer,而且將定時器添加到RunLoop中進行處理。整個流程大體以下:

  • 首先點擊屏幕,其實就是接收到一個Source1事件,由Source1喚醒RunLoop,而後將事件傳遞給Source0
  • RunLoop被喚醒以後會從新進入循環,所以首先會處理Source0事件,所以任務1和任務3會被執行
  • 任務2則是經過添加NSTimer的方式來執行,雖然是延遲0s,可是始終是定時器事件,而RunLoop是在被喚醒時纔會去處理NSTimer事件,所以任務2是在RunLoop下一次被喚醒時纔會被執行。

參考文章

RunLoop官方文檔

深刻理解RunLoop | Garan no dou

結束語

以上內容純屬我的理解,若是有什麼不對的地方歡迎留言指正。

一塊兒學習,一塊兒進步~~~

相關文章
相關標籤/搜索