重學OC第二十六篇:RunLoop

SofunNiu發表於2021-01-05

前言

本篇主要是對官方文件和CFRunLoop原始碼的學習。

一、RunLoop解析

RunLoop是執行緒進入的一個事件處理迴圈,用於響應傳入事件並執行事件處理程式。
RunLoop的目的是在有工作要做時讓執行緒忙,而在沒有工作時讓執行緒進入睡眠狀態。
每個執行緒都有一個關聯的RunLoop物件,但只有輔助執行緒需要顯式地執行其執行迴圈。在應用程式啟動過程中應用程式框架會自動在主執行緒上設定並執行執行迴圈。

RunLoop接收兩種型別的Source:

  • Input Sources,傳遞非同步事件。將非同步事件傳遞給相應的處理程式,並導致runUntilDate:方法(線上程的關聯NSRunLoop物件上呼叫)退出
  • Timer Sources,傳遞同步事件。將事件傳遞到其處理程式例程,但不會導致RunLoop退出。
    在這裡插入圖片描述
    除了處理輸入的source,RunLoop還會生成相關行為的通知。

1.1 Modes

RunLoop Mode是要監視的Input source和Timer的集合,以及要通知的RunLoop Observer的集合。
每次執行RunLoop時,都要(顯式或隱式)指定執行的特定“模式”。
在RunLoop的整個過程中,僅監視與該模式關聯的源,並允許其傳遞事件。 (類似地,僅將與該模式關聯的觀察者通知RunLoop的進度。)與其他模式關聯的源將保留任何新事件,直到隨後以適當的模式通過迴圈。
必須確保將一個或多個Input sources、Timer或RunLoop Observer新增到建立的任何模式中,如果RunLoop沒有任何要監視的源,則當你嘗試執行它時,它將立即退出。
在這裡插入圖片描述
模式根據事件的來源而不是事件的型別進行區分。

1.2 Sources

RunLoop接收兩種型別的Source:

  • Input Sources,傳遞非同步事件
  • Timer Sources,傳遞同步事件

1.2.1 Input Sources

輸入事件源有兩種:基於Port的和自定義的。基於埠的輸入源監視應用程式的Mach埠,自定義輸入源監視事件的自定義源。兩種訊號源之間的唯一區別是訊號的傳送方式。基於埠的源由核心自動發出訊號,而自定義源必須從另一個執行緒手動發出訊號。

  • Port-Based Sources
    Cocoa和Core Foundation提供了內建支援,用於使用與埠相關的物件和功能建立基於埠的輸入源。NSPort、CFMachPortRef、CFMessagePortRef、 CFSocketRef等。
  • Custom Input Sources
    若要建立自定義輸入源,必須在Core Foundation中使用與CFRunLoopSourceRef型別關聯的功能。可以使用多個回撥函式配置自定義輸入源。Core Foundation在不同的位置呼叫這些函式來配置源,處理任何傳入事件,從RunLoop中刪除源時拆除源。
    除了定義事件到達時自定義源的行為外,還必須定義事件傳遞機制。原始碼的這一部分在單獨的執行緒上執行,負責為輸入源提供其資料,並在準備好處理資料時向其發出訊號。
    • Cocoa Perform Selector Sources
      Cocoa定義的一個自定義輸入源,該源可在任何執行緒上執行選擇器。與基於埠的源類似,執行選擇器請求在目標執行緒上序列化,從而減輕了在一個執行緒上執行多種方法時可能發生的許多同步問題。與基於埠的源不同,執行選擇器源在執行選擇器後將其自身從執行迴圈中刪除。
      在另一個執行緒上執行選擇器時,目標執行緒必須具有活動的執行迴圈。每次迴圈時,執行迴圈都會處理所有排隊的執行選擇器呼叫,而不是在每次迴圈迭代時都處理一個。這些perform方法實際上並不建立新的執行緒來執行選擇器。

1.2.2 Timer Sources

計時器不是實時機制,它與RunLoop的特定模式相關聯。如果計時器不在RunLoop當前正在監視的模式下,則只有在以計時器支援的一種模式執行RunLoop後,計時器才會觸發。同樣,如果RunLoop在執行處理程式例程的中間觸發計時器,則計時器將等到下一次通過RunLoop呼叫其處理例程。如果RunLoop根本沒有執行,則計時器永遠不會觸發。
可以將計時器配置為僅一次或重複生成事件。重複計時器會根據計劃的觸發時間(而不是實際的觸發時間)自動重新計劃自身。例如,如果計劃將計時器在特定時間觸發,然後每5秒觸發一次,則即使實際觸發時間被延遲,計劃的觸發時間也將始終落在原始的5秒時間間隔上。如果觸發時間延遲得太多,以至於錯過了一個或多個計劃的觸發時間,則計時器將在錯過的時間段內僅觸發一次。在錯過了一段時間後觸發之後,計時器將重新安排為下一個計劃的觸發時間。

1.3 Observers

Observers在RunLoop本身執行期間的特定位置觸發。可以將Observer與RunLoop中的以下事件相關聯:

  • 執行迴圈的進入
  • 當執行迴圈將要處理計時器
  • 當執行迴圈將要處理輸入源
  • 當執行迴圈即將進入睡眠狀態時
  • 當執行迴圈喚醒時,但在處理該事件之前將其喚醒
  • 執行迴圈的退出

1.4 事件執行順序

每次執行它時,執行緒的RunLoop都會處理未決事件,併為所有關心的觀察者生成通知。它的執行順序如下所示:

  1. 通知Observers已進入RunLoop。
  2. 通知Observers任何準備就緒的Timer即將觸發。
  3. 通知Observers任何不基於埠的Input sources都將被觸發。
  4. 觸發所有準備觸發的非基於埠的輸入源。
  5. 如果基於埠的輸入源已準備好並等待啟動,請立即處理事件。轉到步驟9。
  6. 通知觀察者執行緒即將進入睡眠狀態
  7. 使執行緒進入睡眠狀態,直到發生以下事件之一:
    • 事件到達基於埠的輸入源
    • 計時器觸發
    • 為RunLoop設定的超時值到期
    • RunLoop被顯式喚醒
  8. 通知觀察者執行緒剛剛醒來
  9. 處理未決事件
    • 如果觸發了使用者定義的計時器,請處理計時器事件並重新啟動迴圈。轉到步驟2
    • 如果觸發了輸入源,請傳遞事件
    • 如果執行迴圈已顯式喚醒,但尚未超時,請重新啟動迴圈。轉到步驟2
  10. 通知Observers已退出RunLoop。

1.5 何時使用RunLoop

主執行緒會自動執行,對於輔助執行緒,需要確定是否需要RunLoop,如果需要,請自行配置並啟動它。執行迴圈用於需要與執行緒進行更多互動的情況,比如執行以下任一操作,則需要啟動執行迴圈:

  • 使用埠或自定義輸入源與其他執行緒進行通訊。
  • 線上程上使用計時器
  • 在Cocoa應用程式中使用任何performSelector…方法
  • 保持執行緒執行定期任務

二、CFRunLoop原始碼

2.1 RunLoop對應的CF類

在 CoreFoundation 裡面關於 RunLoop 有5個類: CFRunLoopRef、CFRunLoopModeRef(沒對外暴露)、CFRunLoopSourceRef、CFRunLoopTimerRef、CFRunLoopObserverRef。
它們的關係如下:
在這裡插入圖片描述
一個 RunLoop 包含若干個 Mode,每個 Mode 包含若干個Source/Timer/Observer。每次呼叫 RunLoop 的主函式時,只能指定其中一個 Mode,這個Mode被稱作 CurrentMode。如果需要切換 Mode,只能退出 Loop,再重新指定一個 Mode 進入。這樣做主要是為了分隔開不同組的 Source/Timer/Observer,讓其互不影響。

2.1.1 CFRunLoopRef

typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef;
//__CFRunLoop部分程式碼
struct __CFRunLoop {  
    ......
    _CFThreadRef _pthread;   //對應的執行緒
    CFMutableSetRef _commonModes;   // common mode中包含的模式
    CFMutableSetRef _commonModeItems; //加入到commonMode中的source、timer、observer
    CFRunLoopModeRef _currentMode; //當前執行的模式
    CFMutableSetRef _modes;  //RunLoop中的模式
 	......
};

通過CFRunLoopCopyCurrentMode()和CFRunLoopCopyAllModes()函式可檢視RunLoop的當前模式和modes中的所有模式。

2.1.2 CFRunLoopModeRef

typedef struct __CFRunLoopMode *CFRunLoopModeRef;
//__CFRunLoopMode部分程式碼
struct __CFRunLoopMode {
    CFStringRef _name;
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    ......
};

2.1.3 CFRunLoopSourceRef

typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoopSource * CFRunLoopSourceRef;

struct __CFRunLoopSource {
    CFRuntimeBase _base;
    _CFRecursiveMutex _lock;
    CFIndex _order;			/* immutable */
    CFMutableBagRef _runLoops;
    union {
	CFRunLoopSourceContext version0;	/* immutable, except invalidation */
        CFRunLoopSourceContext1 version1;	/* immutable, except invalidation */
    } _context;
    _Atomic(Boolean) _signaled;
};

typedef struct {
    ......
    void	(*schedule)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
    void	(*cancel)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
    void	(*perform)(void *info);
} CFRunLoopSourceContext;

typedef struct {
   	......
    mach_port_t	(*getPort)(void *info);
    void *	(*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
} CFRunLoopSourceContext1;

CFRunLoopSourceRef 是Input Sources事件產生的地方。Source有兩個版本:Source0 和 Source1。

  • Source0 只包含了一個回撥(函式指標),它並不能主動觸發事件。使用時,你需要先呼叫 CFRunLoopSourceSignal(source),將這個 Source 標記為待處理,然後手動呼叫 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件。
  • Source1包含了一個 mach_port 和一個回撥(函式指標),被用於通過核心和其他執行緒相互傳送訊息,這種 Source 能主動喚醒 RunLoop 的執行緒

2.1.4 CFRunLoopTimerRef

typedef struct CF_BRIDGED_MUTABLE_TYPE(NSTimer) __CFRunLoopTimer * CFRunLoopTimerRef;

struct __CFRunLoopTimer {
    CFRuntimeBase _base;
    uint16_t _bits;
    _CFRecursiveMutex _lock;
    CFRunLoopRef _runLoop;
    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 */
};

CFRunLoopTimerRef 是基於時間的觸發器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一個時間長度和一個回撥(函式指標)。當其加入到 RunLoop 時,RunLoop會註冊對應的時間點,當時間點到時,RunLoop會被喚醒以執行那個回撥。

2.1.5 CFRunLoopObserverRef

typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoopObserver * CFRunLoopObserverRef;

struct __CFRunLoopObserver {
    CFRuntimeBase _base;
    _CFRecursiveMutex _lock;
    CFRunLoopRef _runLoop;
    CFIndex _rlCount;
    CFOptionFlags _activities;		/* immutable */
    CFIndex _order;			/* immutable */
    CFRunLoopObserverCallBack _callout;	/* immutable */
    CFRunLoopObserverContext _context;	/* immutable, except invalidation */
};

CFRunLoopObserverRef 是觀察者,每個 Observer 都包含了一個回撥(函式指標),當 RunLoop 的狀態發生變化時,觀察者就能通過回撥接受到這個變化。可以觀測的時間點有以下幾個:
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
};

2.2 RunLoop事件執行

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

主要是CFRunLoopRunSpecific函式

SInt32 CFRunLoopRunSpecific(...) {   
	...
	//1、進入Runloop
	if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
	result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
	//10、從Runloop退出
	if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}

//下面程式碼精簡了,僅供參考
static int32_t __CFRunLoopRun(...) {
	do {
		...
		//2、即將觸發已就緒的Timers
		if (rlm->_observerMask & kCFRunLoopBeforeTimers) {
	    	__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
		}
		//3、即將觸發Source0(非基於port)
		if (rlm->_observerMask & kCFRunLoopBeforeSources) {
	    	__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
		}	
		//執行加入的block
		__CFRunLoopDoBlocks(rl, rlm);
		//4、處理Source0事件
		Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
	    if (sourceHandledThisLoop) {
	       __CFRunLoopDoBlocks(rl, rlm);
	    }
	    Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
	    if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
	    	msg = (mach_msg_header_t *)msg_buffer;
	    	//5、如果基於埠的輸入源已準備好並等待啟動,請立即處理事件。轉到步驟9
	    	if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL, rl, rlm)) {
	            goto handle_msg;
	        }
	    }
	    //6、通知觀察者執行緒即將進入睡眠狀態。
	    if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
	    __CFRunLoopSetSleeping(rl);
		//7、使執行緒進入睡眠狀態,直到發生以下事件之一:
		//-->事件到達基於埠的輸入源
		//-->計時器觸發
		//-->為執行迴圈設定的超時值到期
		//-->執行迴圈被明確喚醒
	    __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy, rl, rlm);
	    //8、通知觀察者執行緒剛剛醒來
	    if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
	    //9、處理未決事件
	    handle_msg:;
	    if (MACH_PORT_NULL == livePort) {
	    	CFRUNLOOP_WAKEUP_FOR_NOTHING();  //沒事情需要處理
	    } else if (livePort == rl->_wakeUpPort) {
	    	CFRUNLOOP_WAKEUP_FOR_WAKEUP();  //
	    } else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
	    	//9.1 如果觸發了使用者定義的計時器,請處理計時器事件
	    	CFRUNLOOP_WAKEUP_FOR_TIMER();
	    	__CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
		} else if (livePort == dispatchPort) { 
			//9.2 處理非同步方法喚醒,如dispatch_async
			CFRUNLOOP_WAKEUP_FOR_DISPATCH();
			__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
		} else {
			//9.3 如果一個 Source1 (基於port) 發出事件了,處理這個事件
			CFRUNLOOP_WAKEUP_FOR_SOURCE();
			CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
			sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
			(void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
		}
		//執行加入到Runloop的block
		__CFRunLoopDoBlocks(rl, rlm);
		
		if (sourceHandledThisLoop && stopAfterHandle) {
			//處理完事件就返回
		    retVal = kCFRunLoopRunHandledSource;
	    } else if (timeout_context->termTSR < mach_absolute_time()) {
	    	//超出傳入引數標記的超時時間
	        retVal = kCFRunLoopRunTimedOut;
		} else if (__CFRunLoopIsStopped(rl)) {
			//被外部呼叫者強制停止了
	        __CFRunLoopUnsetStopped(rl);
		    retVal = kCFRunLoopRunStopped;
		} else if (rlm->_stopped) {	
		    rlm->_stopped = false;
		    retVal = kCFRunLoopRunStopped;
		} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
			//mode中不存在source/timer/observer
		    retVal = kCFRunLoopRunFinished;
		}
	} while (0 == retVal);
	
}

在這裡插入圖片描述
Runloop的休眠呼叫了__CFRunLoopServiceMachPort方法,它裡面其實呼叫的是mach_msg,而mach_msg又通過mach_msg_trap實現了使用者態核心態之間的轉換。

在對事件進行處理時會呼叫__CFRunLoopDoTimers、__CFRunLoopDoObservers等函式,而系統在其中回撥時通常使用如下幾個函式進行回撥:

static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();  //observer
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();  //block
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(); //main_dispatch_queue
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(); //timer
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(); //source0
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(); //source1

總結

主執行緒會自動建立執行Runloop,而輔助執行緒Runloop需要自行配置並啟動才能起作用。


參考文章:
官方runloop地址
ibireme:深入理解RunLoop 轉載備用連結
iOS刨根問底-深入理解RunLoop

相關文章