Runloop 是和線程緊密相關的一個基礎組件,是不少線程有關功能的幕後功臣。儘管在日常使用中幾乎不太會直接用到,理解 Runloop 有利於咱們更加深刻地理解 iOS 的多線程模型。php
本文從以下幾個方面理解RunLoop的相關知識點。html
RunLoop 是什麼?RunLoop 仍是比較顧名思義的一個東西,說白了就是一種循環,只不過它這種循環比較高級。通常的 while 循環會致使 CPU 進入忙等待狀態,而 RunLoop 則是一種「閒」等待,這部分能夠類比 Linux 下的 epoll。當沒有事件時,RunLoop 會進入休眠狀態,有事件發生時, RunLoop 會去找對應的 Handler 處理事件。RunLoop 可讓線程在須要作事的時候忙起來,不須要的話就讓線程休眠。git
從代碼上看,RunLoop其實就是一個對象,它的結構以下,源碼看這裏:github
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp 內核向該端口發送消息能夠喚醒runloop
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread; //RunLoop對應的線程
uint32_t _winthread;
CFMutableSetRef _commonModes; //存儲的是字符串,記錄全部標記爲common的mode
CFMutableSetRef _commonModeItems;//存儲全部commonMode的item(source、timer、observer)
CFRunLoopModeRef _currentMode; //當前運行的mode
CFMutableSetRef _modes; //存儲的是CFRunLoopModeRef
struct _block_item *_blocks_head;//doblocks的時候用到
struct _block_item *_blocks_tail;
CFTypeRef _counterpart;
};
複製代碼
可見,一個RunLoop對象,主要包含了一個線程,若干個Mode,若干個commonMode,還有一個當前運行的Mode。windows
當咱們須要一個常駐線程,可讓線程在須要作事的時候忙起來,不須要的話就讓線程休眠。咱們就在線程裏面執行下面這個代碼,一直等待消息,線程就不會退出了。數組
do {
//獲取消息
//處理消息
} while (消息 != 退出)
複製代碼
上面的這種循環模型被稱做 Event Loop,事件循環模型在衆多系統裏都有實現,RunLoop 實際上就是一個對象,這個對象管理了其須要處理的事件和消息,並提供了一個入口函數來執行上面 Event Loop 的邏輯。線程執行了這個函數後,就會一直處於這個函數內部 "接受消息->等待->處理" 的循環中,直到這個循環結束(好比傳入 quit 的消息),函數返回。緩存
下圖描述了Runloop運行流程(基本描述了上面Runloop的核心流程,固然能夠查看官方The Run Loop Sequence of Events描述):bash
整個流程並不複雜(須要注意的就是_黃色_區域的消息處理中並不包含source0,由於它在循環開始之初就會處理),整個流程其實就是一種Event Loop的實現,其餘平臺均有相似的實現,只是這裏叫作RunLoop。微信
RunLoop與線程的關係以下圖cookie
圖中展示了 Runloop 在線程中的做用:從 input source 和 timer source 接受事件,而後在線程中處理事件。
Runloop 和線程是綁定在一塊兒的。每一個線程(包括主線程)都有一個對應的 Runloop 對象。咱們並不能本身建立 Runloop 對象,可是能夠獲取到系統提供的 Runloop 對象。
主線程的 Runloop 會在應用啓動的時候完成啓動,其餘線程的 Runloop 默認並不會啓動,須要咱們手動啓動。
Mode能夠視爲事件的管家,一個Mode管理着各類事件,它的結構以下:
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name; //mode名稱
Boolean _stopped; //mode是否被終止
char _padding[3];
//幾種事件
CFMutableSetRef _sources0; //sources0
CFMutableSetRef _sources1; //sources1
CFMutableArrayRef _observers; //通知
CFMutableArrayRef _timers; //定時器
CFMutableDictionaryRef _portToV1SourceMap; //字典 key是mach_port_t,value是CFRunLoopSourceRef
__CFPortSet _portSet; //保存全部須要監聽的port,好比_wakeUpPort,_timerPort都保存在這個數組中
CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
dispatch_source_t _timerSource;
dispatch_queue_t _queue;
Boolean _timerFired; // set to true by the source when a timer has fired
Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
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對象有一個name,若干source0、source一、timer、observer和若干port,可見事件都是由Mode在管理,而RunLoop管理Mode。
從源碼很容易看出,Runloop老是運行在某種特定的CFRunLoopModeRef下(每次運行**__CFRunLoopRun()函數時必須指定Mode)。而經過CFRunloopRef對應結構體的定義能夠很容易知道每種Runloop均可以包含若干個Mode,每一個Mode又包含Source/Timer/Observer。每次調用Runloop的主函數__CFRunLoopRun()時必須指定一種Mode,這個Mode稱爲 _currentMode**,當切換Mode時必須退出當前Mode,而後從新進入Runloop以保證不一樣Mode的Source/Timer/Observer互不影響。
如圖所示,Runloop Mode 其實是 Source,Timer 和 Observer 的集合,不一樣的 Mode 把不一樣組的 Source,Timer 和 Observer 隔絕開來。Runloop 在某個時刻只能跑在一個 Mode 下,處理這一個 Mode 當中的 Source,Timer 和 Observer。
蘋果文檔中提到的 Mode 有五個,分別是:
iOS 中公開暴露出來的只有 NSDefaultRunLoopMode 和 NSRunLoopCommonModes。 NSRunLoopCommonModes 其實是一個 Mode 的集合,默認包括 NSDefaultRunLoopMode 和 NSEventTrackingRunLoopMode(注意:並非說Runloop會運行在kCFRunLoopCommonModes這種模式下,而是至關於分別註冊了 NSDefaultRunLoopMode和 UITrackingRunLoopMode。固然你也能夠經過調用CFRunLoopAddCommonMode()方法將自定義Mode放到 kCFRunLoopCommonModes組合)。
五種Mode的介紹以下圖:
Run Loop Source分爲Source、Observer、Timer三種,他們統稱爲ModeItem。
根據官方的描述,CFRunLoopSource是對input sources的抽象。CFRunLoopSource分source0和source1兩個版本,它的結構以下:
struct __CFRunLoopSource {
CFRuntimeBase _base;
uint32_t _bits; //用於標記Signaled狀態,source0只有在被標記爲Signaled狀態,纔會被處理
pthread_mutex_t _lock;
CFIndex _order; /* immutable */
CFMutableBagRef _runLoops;
union {
CFRunLoopSourceContext version0; /* immutable, except invalidation */
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
} _context;
};
複製代碼
source0是App內部事件,由App本身管理的UIEvent、CFSocket都是source0。當一個source0事件準備執行的時候,必需要先把它標記爲signal狀態,如下是source0的結構體:
typedef struct {
CFIndex version;
void * info;
const void *(*retain)(const void *info);
void (*release)(const void *info);
CFStringRef (*copyDescription)(const void *info);
Boolean (*equal)(const void *info1, const void *info2);
CFHashCode (*hash)(const void *info);
void (*schedule)(void *info, CFRunLoopRef rl, CFStringRef mode);
void (*cancel)(void *info, CFRunLoopRef rl, CFStringRef mode);
void (*perform)(void *info);
} CFRunLoopSourceContext;
複製代碼
source0是非基於Port的。只包含了一個回調(函數指針),它並不能主動觸發事件。使用時,你須要先調用 CFRunLoopSourceSignal(source),將這個 Source 標記爲待處理,而後手動調用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件。
source1由RunLoop和內核管理,source1帶有mach_port_t,能夠接收內核消息並觸發回調,如下是source1的結構體
typedef struct {
CFIndex version;
void * info;
const void *(*retain)(const void *info);
void (*release)(const void *info);
CFStringRef (*copyDescription)(const void *info);
Boolean (*equal)(const void *info1, const void *info2);
CFHashCode (*hash)(const void *info);
#if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) || (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)
mach_port_t (*getPort)(void *info);
void * (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
#else
void * (*getPort)(void *info);
void (*perform)(void *info);
#endif
} CFRunLoopSourceContext1;
複製代碼
Source1除了包含回調指針外包含一個mach port,Source1能夠監聽系統端口和經過內核和其餘線程通訊,接收、分發系統事件,它可以主動喚醒RunLoop(由操做系統內核進行管理,例如CFMessagePort消息)。官方也指出能夠自定義Source,所以對於CFRunLoopSourceRef來講它更像一種協議,框架已經默認定義了兩種實現,若是有必要開發人員也能夠自定義,詳細狀況能夠查看官方文檔。
CFRunLoopObserver是觀察者,能夠觀察RunLoop的各類狀態,並拋出回調。
struct __CFRunLoopObserver {
CFRuntimeBase _base;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop;
CFIndex _rlCount;
CFOptionFlags _activities; /* immutable */
CFIndex _order; /* immutable */
CFRunLoopObserverCallBack _callout; /* immutable */
CFRunLoopObserverContext _context; /* immutable, except invalidation */
};
複製代碼
CFRunLoopObserver能夠觀察的狀態有以下6種:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), //即將進入run loop
kCFRunLoopBeforeTimers = (1UL << 1), //即將處理timer
kCFRunLoopBeforeSources = (1UL << 2),//即將處理source
kCFRunLoopBeforeWaiting = (1UL << 5),//即將進入休眠
kCFRunLoopAfterWaiting = (1UL << 6),//被喚醒可是還沒開始處理事件
kCFRunLoopExit = (1UL << 7),//run loop已經退出
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
複製代碼
Runloop 經過監控 Source 來決定有沒有任務要作,除此以外,咱們還能夠用 Runloop Observer 來監控 Runloop 自己的狀態。 Runloop Observer 能夠監控上面的 Runloop 事件,具體流程以下圖。
CFRunLoopTimer是定時器,能夠在設定的時間點拋出回調,它的結構以下:
struct __CFRunLoopTimer {
CFRuntimeBase _base;
uint16_t _bits; //標記fire狀態
pthread_mutex_t _lock;
CFRunLoopRef _runLoop; //添加該timer的runloop
CFMutableSetRef _rlModes; //存放全部 包含該timer的 mode的 modeName,意味着一個timer可能會在多個mode中存在
CFAbsoluteTime _nextFireDate;
CFTimeInterval _interval; //理想時間間隔 /* immutable */
CFTimeInterval _tolerance; //時間誤差 /* mutable */
uint64_t _fireTSR; /* TSR units */
CFIndex _order; /* immutable */
CFRunLoopTimerCallBack _callout; /* immutable */
CFRunLoopTimerContext _context; /* immutable, except invalidation */
};
複製代碼
另外根據官方文檔的描述,CFRunLoopTimer和NSTimer是toll-free bridged的,能夠相互轉換。
CFRunLoopTimer is 「toll-free bridged」 with its Cocoa Foundation counterpart, NSTimer. This means that the Core Foundation type is interchangeable in function or method calls with the bridged Foundation object.
因此CFRunLoopTimer具備如下特性:
下面從如下3個方面介紹RunLoop的實現。
從蘋果開放的API來看,不容許咱們直接建立RunLoop對象,只能經過如下幾個函數來獲取RunLoop:
前兩個是Core Foundation中的API,後兩個是Foundation中的API。
那麼RunLoop是何時被建立的呢?
咱們從下面幾個函數內部看看。
//取當前所在線程的RunLoop
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。也就是說子線程的RunLoop必需要在子線程內部獲取。
//取主線程的RunLoop
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()
。能夠看出,CFRunLoopGetMain()無論在主線程仍是子線程中調用,均可以獲取到主線程的RunLoop。
前面兩個函數都是使用了CFRunLoopGet0實現傳入線程的函數,下面看下CFRunLoopGet0的結構是咋樣的。
static CFMutableDictionaryRef __CFRunLoops = NULL;
static CFSpinLock_t loopsLock = CFSpinLockInit;
// t==0 is a synonym for "main thread" that always works
//根據線程取RunLoop
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFSpinLock(&loopsLock);
//若是存儲RunLoop的字典不存在
if (!__CFRunLoops) {
__CFSpinUnlock(&loopsLock);
//建立一個臨時字典dict
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)) {
//釋放dict
CFRelease(dict);
}
//釋放mainrunloop
CFRelease(mainLoop);
__CFSpinLock(&loopsLock);
}
//以上說明,第一次進來的時候,無論是getMainRunloop仍是get子線程的runloop,主線程的runloop老是會被建立
//從字典__CFRunLoops中獲取傳入線程t的runloop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFSpinUnlock(&loopsLock);
//若是沒有獲取到
if (!loop) {
//根據線程t建立一個runloop
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFSpinLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
//把newLoop存入字典__CFRunLoops,key是線程t
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it __CFSpinUnlock(&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; } 複製代碼
這段代碼能夠得出如下結論:
在Core Foundation中,針對Mode的操做,蘋果只開放了如下3個API(Cocoa中也有功能同樣的函數,再也不列出):
CFRunLoopAddCommonMode Adds a mode to the set of run loop common modes. 向當前RunLoop的common modes中添加一個mode。
CFRunLoopCopyCurrentMode Returns the name of the mode in which a given run loop is currently running. 返回當前運行的mode的name
CFRunLoopCopyAllModes Returns an array that contains all the defined modes for a CFRunLoop object. 返回當前RunLoop的全部mode
咱們沒有辦法直接建立一個CFRunLoopMode對象,可是咱們能夠調用CFRunLoopAddCommonMode傳入一個字符串向RunLoop中添加Mode,傳入的字符串即爲Mode的名字,Mode對象應該是此時在RunLoop內部建立的。下面來看一下源碼。
void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName) {
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return;
__CFRunLoopLock(rl);
//看rl中是否已經有這個mode,若是有就什麼都不作
if (!CFSetContainsValue(rl->_commonModes, modeName)) {
CFSetRef set = rl->_commonModeItems ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModeItems) : NULL;
//把modeName添加到RunLoop的_commonModes中
CFSetAddValue(rl->_commonModes, modeName);
if (NULL != set) {
CFTypeRef context[2] = {rl, modeName};
/* add all common-modes items to new mode */
//這裏調用CFRunLoopAddSource/CFRunLoopAddObserver/CFRunLoopAddTimer的時候會調用
//__CFRunLoopFindMode(rl, modeName, true),CFRunLoopMode對象在這個時候被建立
CFSetApplyFunction(set, (__CFRunLoopAddItemsToCommonMode), (void *)context);
CFRelease(set);
}
} else {
}
__CFRunLoopUnlock(rl);
}
複製代碼
能夠看得出:
CFRunLoopCopyCurrentMode和CFRunLoopCopyAllModes的內部邏輯比較簡單,直接取RunLoop的_currentMode和_modes返回,就不貼源碼了。
咱們能夠經過如下接口添加/移除各類事件:
CFRunLoopAddSource的代碼結構以下:
//添加source事件
void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) { /* DOES CALLOUT */
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return;
if (!__CFIsValid(rls)) return;
Boolean doVer0Callout = false;
__CFRunLoopLock(rl);
//若是是kCFRunLoopCommonModes
if (modeName == kCFRunLoopCommonModes) {
//若是runloop的_commonModes存在,則copy一個新的複製給set
CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
//若是runl _commonModeItems爲空
if (NULL == rl->_commonModeItems) {
//先初始化
rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
}
//把傳入的CFRunLoopSourceRef加入_commonModeItems
CFSetAddValue(rl->_commonModeItems, rls);
//若是剛纔set copy到的數組裏有數據
if (NULL != set) {
CFTypeRef context[2] = {rl, rls};
/* add new item to all common-modes */
//則把set裏的全部mode都執行一遍__CFRunLoopAddItemToCommonModes函數
CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
CFRelease(set);
}
//以上分支的邏輯就是,若是你往kCFRunLoopCommonModes裏面添加一個source,那麼全部_commonModes裏的mode都會添加這個source
} else {
//根據modeName查找mode
CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true);
//若是_sources0不存在,則初始化_sources0,_sources0和_portToV1SourceMap
if (NULL != rlm && NULL == rlm->_sources0) {
rlm->_sources0 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
rlm->_sources1 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
rlm->_portToV1SourceMap = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, NULL);
}
//若是_sources0和_sources1中都不包含傳入的source
if (NULL != rlm && !CFSetContainsValue(rlm->_sources0, rls) && !CFSetContainsValue(rlm->_sources1, rls)) {
//若是version是0,則加到_sources0
if (0 == rls->_context.version0.version) {
CFSetAddValue(rlm->_sources0, rls);
//若是version是1,則加到_sources1
} else if (1 == rls->_context.version0.version) {
CFSetAddValue(rlm->_sources1, rls);
__CFPort src_port = rls->_context.version1.getPort(rls->_context.version1.info);
if (CFPORT_NULL != src_port) {
//此處只有在加到source1的時候纔會把souce和一個mach_port_t對應起來
//能夠理解爲,source1能夠經過內核向其端口發送消息來主動喚醒runloop
CFDictionarySetValue(rlm->_portToV1SourceMap, (const void *)(uintptr_t)src_port, rls);
__CFPortSetInsert(src_port, rlm->_portSet);
}
}
__CFRunLoopSourceLock(rls);
//把runloop加入到source的_runLoops中
if (NULL == rls->_runLoops) {
rls->_runLoops = CFBagCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeBagCallBacks); // sources retain run loops!
}
CFBagAddValue(rls->_runLoops, rl);
__CFRunLoopSourceUnlock(rls);
if (0 == rls->_context.version0.version) {
if (NULL != rls->_context.version0.schedule) {
doVer0Callout = true;
}
}
}
if (NULL != rlm) {
__CFRunLoopModeUnlock(rlm);
}
}
__CFRunLoopUnlock(rl);
if (doVer0Callout) {
// although it looses some protection for the source, we have no choice but
// to do this after unlocking the run loop and mode locks, to avoid deadlocks
// where the source wants to take a lock which is already held in another
// thread which is itself waiting for a run loop/mode lock
rls->_context.version0.schedule(rls->_context.version0.info, rl, modeName); /* CALLOUT */
}
}
複製代碼
經過添加source的這段代碼能夠得出以下結論:
remove操做和add操做的邏輯基本一致,很容易理解。
//移除source
void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) { /* DOES CALLOUT */
CHECK_FOR_FORK();
Boolean doVer0Callout = false, doRLSRelease = false;
__CFRunLoopLock(rl);
//若是是kCFRunLoopCommonModes,則從_commonModes的全部mode中移除該source
if (modeName == kCFRunLoopCommonModes) {
if (NULL != rl->_commonModeItems && CFSetContainsValue(rl->_commonModeItems, rls)) {
CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
CFSetRemoveValue(rl->_commonModeItems, rls);
if (NULL != set) {
CFTypeRef context[2] = {rl, rls};
/* remove new item from all common-modes */
CFSetApplyFunction(set, (__CFRunLoopRemoveItemFromCommonModes), (void *)context);
CFRelease(set);
}
} else {
}
} else {
//根據modeName查找mode,若是不存在,返回NULL
CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, false);
if (NULL != rlm && ((NULL != rlm->_sources0 && CFSetContainsValue(rlm->_sources0, rls)) || (NULL != rlm->_sources1 && CFSetContainsValue(rlm->_sources1, rls)))) {
CFRetain(rls);
//根據source版本作對應的remove操做
if (1 == rls->_context.version0.version) {
__CFPort src_port = rls->_context.version1.getPort(rls->_context.version1.info);
if (CFPORT_NULL != src_port) {
CFDictionaryRemoveValue(rlm->_portToV1SourceMap, (const void *)(uintptr_t)src_port);
__CFPortSetRemove(src_port, rlm->_portSet);
}
}
CFSetRemoveValue(rlm->_sources0, rls);
CFSetRemoveValue(rlm->_sources1, rls);
__CFRunLoopSourceLock(rls);
if (NULL != rls->_runLoops) {
CFBagRemoveValue(rls->_runLoops, rl);
}
__CFRunLoopSourceUnlock(rls);
if (0 == rls->_context.version0.version) {
if (NULL != rls->_context.version0.cancel) {
doVer0Callout = true;
}
}
doRLSRelease = true;
}
if (NULL != rlm) {
__CFRunLoopModeUnlock(rlm);
}
}
__CFRunLoopUnlock(rl);
if (doVer0Callout) {
// although it looses some protection for the source, we have no choice but
// to do this after unlocking the run loop and mode locks, to avoid deadlocks
// where the source wants to take a lock which is already held in another
// thread which is itself waiting for a run loop/mode lock
rls->_context.version0.cancel(rls->_context.version0.info, rl, modeName); /* CALLOUT */
}
if (doRLSRelease) CFRelease(rls);
}
複製代碼
添加observer和timer的內部邏輯和添加source大致相似。
區別在於observer和timer只能被添加到一個RunLoop的一個或者多個mode中,好比一個timer被添加到主線程的RunLoop中,則不能再把該timer添加到子線程的RunLoop,而source沒有這個限制,無論是哪一個RunLoop,只要mode中沒有,就能夠添加。
這個區別在文章最開始的結構體中也能夠發現,CFRunLoopSource結構體中有保存RunLoop對象的數組,而CFRunLoopObserver和CFRunLoopTimer只有單個RunLoop對象。
在Core Foundation中咱們能夠經過如下2個API來讓RunLoop運行:
在默認的mode下運行當前線程的RunLoop。
在指定mode下運行當前線程的RunLoop。
//默認運行runloop的kCFRunLoopDefaultMode
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
//默認在kCFRunLoopDefaultMode下運行runloop
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
複製代碼
在CFRunLoopRun函數中調用了CFRunLoopRunSpecific函數,runloop參數傳入當前RunLoop對象,modeName參數傳入kCFRunLoopDefaultMode。驗證了前面文檔的解釋。
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
複製代碼
在CFRunLoopRunInMode函數中也調用了CFRunLoopRunSpecific函數,runloop參數傳入當前RunLoop對象,modeName參數繼續傳遞CFRunLoopRunInMode傳入的modeName。也驗證了前面文檔的解釋。
這裏還能夠看出,雖然RunLoop有不少個mode,可是RunLoop在run的時候必須只能指定其中一個mode,運行起來以後,被指定的mode即爲currentMode。
這2個函數都看不出來RunLoop是怎麼run起來的。
接下來咱們繼續探索一下CFRunLoopRunSpecific函數裏面都幹了什麼,看看RunLoop具體是怎麼run的。
/*
* 指定mode運行runloop
* @param rl 當前運行的runloop
* @param modeName 須要運行的mode的name
* @param seconds runloop的超時時間
* @param returnAfterSourceHandled 是否處理完事件就返回
*/
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中沒有註冊任何事件,則就此中止,不進入循環
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;
//若是本次mode和上次的mode一致
rl->_currentMode = currentMode;
//初始化一個result爲kCFRunLoopRunFinished
int32_t result = kCFRunLoopRunFinished;
// 1.通知observer即將進入runloop
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
//10.通知observer已退出runloop
if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
__CFRunLoopModeUnlock(currentMode);
__CFRunLoopPopPerRunData(rl, previousPerRun);
rl->_currentMode = previousMode;
__CFRunLoopUnlock(rl);
return result;
}
複製代碼
經過CFRunLoopRunSpecific的內部邏輯,咱們能夠得出:
RunLoop的運行的最核心函數是__CFRunLoopRun,接下來咱們分析__CFRunLoopRun的源碼。
這段代碼比較長,請作好心理準備,我已經加了比較詳細的註釋。本節開頭的run loop運行步驟2~9步都在下面的代碼中獲得驗證。
/**
* 運行run loop
*
* @param rl 運行的RunLoop對象
* @param rlm 運行的mode
* @param seconds run loop超時時間
* @param stopAfterHandle true:run loop處理完事件就退出 false:一直運行直到超時或者被手動終止
* @param previousMode 上一次運行的mode
*
* @return 返回4種狀態
*/
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; } 複製代碼
第7步調用了__CFRunLoopServiceMachPort函數,這個函數在run loop中起到了相當重要的做用,下面給出了詳細註釋。
/**
* 接收指定內核端口的消息
*
* @param port 接收消息的端口
* @param buffer 消息緩衝區
* @param buffer_size 消息緩衝區大小
* @param livePort 暫且理解爲活動的端口,接收消息成功時候值爲msg->msgh_local_port,超時時爲MACH_PORT_NULL
* @param timeout 超時時間,單位是ms,若是超時,則RunLoop進入休眠狀態
*
* @return 接收消息成功時返回true 其餘狀況返回false
*/
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;
}
複製代碼
RunLoop實際很簡單,它是一個對象,它和線程是一一對應的,每一個線程都有一個對應的RunLoop對象,主線程的RunLoop會在程序啓動時自動建立,子線程須要手動獲取來建立。
RunLoop運行的核心是一個do..while..循環,遍歷全部須要處理的事件,若是有事件處理就讓線程工做,沒有事件處理則讓線程休眠,同時等待事件到來。
在開發過程當中幾乎全部的操做都是經過Call out進行回調的(不管是Observer的狀態通知仍是Timer、Source的處理),而系統在回調時一般使用以下幾個函數進行回調(換句話說你的代碼其實最終都是經過下面幾個函數來負責調用的,即便你本身監聽Observer也會先調用下面的函數而後間接通知你,因此在調用堆棧中常常看到這些函數):
static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();
複製代碼
實際的代碼塊以下:
{
/// 1. 通知Observers,即將進入RunLoop
/// 此處有Observer會建立AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
do {
/// 2. 通知 Observers: 即將觸發 Timer 回調。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: 即將觸發 Source (非基於port的,Source0) 回調。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 4. 觸發 Source0 (非基於port的) 回調。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 6. 通知Observers,即將進入休眠
/// 此處有Observer釋放並新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
/// 7. sleep to wait msg.
mach_msg() -> mach_msg_trap();
/// 8. 通知Observers,線程被喚醒
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
/// 9. 若是是被Timer喚醒的,回調Timer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
/// 9. 若是是被dispatch喚醒的,執行全部調用 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
/// 9. 若是若是Runloop是被 Source1 (基於port的) 的事件喚醒了,處理這個事件
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
} while (...);
/// 10. 通知Observers,即將退出RunLoop
/// 此處有Observer釋放AutoreleasePool: _objc_autoreleasePoolPop();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}
複製代碼
例如在控制器的touchBegin中打入斷點查看堆棧(因爲UIEvent是Source0,因此能夠看到一個Source0的Call out函數CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION調用):
前面一直提到Timer Source做爲事件源,事實上它的上層對應就是NSTimer(其實就是CFRunloopTimerRef)這個開發者常常用到的定時器(底層基於使用mk_timer實現)
NSTimer 其實就是 CFRunLoopTimerRef,他們之間是 toll-free bridged 的。一個 NSTimer 註冊到 RunLoop 後,RunLoop 會爲其重複的時間點註冊好事件。例如 10:00, 10:10, 10:20 這幾個時間點。RunLoop爲了節省資源,並不會在很是準確的時間點回調這個Timer。Timer 有個屬性叫作 Tolerance (寬容度),標示了當時間點到後,允許有多少最大偏差。因爲 NSTimer 的這種機制,所以 NSTimer 的執行必須依賴於 RunLoop,若是沒有 RunLoop,NSTimer 是不會執行的。
若是某個時間點被錯過了,例如執行了一個很長的任務,則那個時間點的回調也會跳過去,不會延後執行。就好比等公交,若是 10:10 時我忙着玩手機錯過了那個點的公交,那我只能等 10:20 這一趟了。
GCD 則不一樣,GCD 的線程管理是經過系統來直接管理的。GCD Timer 是經過 dispatch port 給 RunLoop 發送消息,來使 RunLoop 執行相應的 block,若是所在線程沒有 RunLoop,那麼 GCD 會臨時建立一個線程去執行 block,執行完以後再銷燬掉,所以 GCD 的 Timer 是不依賴 RunLoop 的。
至於這兩個 Timer 的準確性問題,若是不在 RunLoop 的線程裏面執行,那麼只能使用 GCD Timer,因爲 GCD Timer 是基於 MKTimer(mach kernel timer),已經很底層了,所以是很準確的。
若是在 RunLoop 的線程裏面執行,因爲 GCD Timer 和 NSTimer 都是經過 port 發送消息的機制來觸發 RunLoop 的,所以準確性差異應該不是很大。若是線程 RunLoop 阻塞了,無論是 GCD Timer 仍是 NSTimer 都會存在延遲問題。
CADisplayLink是一個執行頻率(fps)和屏幕刷新相同(能夠修改preferredFramesPerSecond改變刷新頻率)的定時器,它也須要加入到RunLoop才能執行。與NSTimer相似,CADisplayLink一樣是基於CFRunloopTimerRef實現,底層使用mk_timer(能夠比較加入到RunLoop先後RunLoop中timer的變化)。和NSTimer相比它精度更高(儘管NSTimer也能夠修改精度),不過和NStimer相似的是若是遇到大任務它仍然存在丟幀現象。一般狀況下CADisaplayLink用於構建幀動畫,看起來相對更加流暢,而NSTimer則有更普遍的用處。
AutoreleasePool是另外一個與RunLoop相關討論較多的話題。其實從RunLoop源代碼分析,AutoreleasePool與RunLoop並無直接的關係,之因此將兩個話題放到一塊兒討論最主要的緣由是由於在iOS應用啓動後會註冊兩個Observer管理和維護AutoreleasePool。不妨在應用程序剛剛啓動時打印currentRunLoop能夠看到系統默認註冊了不少個Observer,其中有兩個Observer的callout都是** _ wrapRunLoopWithAutoreleasePoolHandler**,這兩個是和自動釋放池相關的兩個監聽。
<CFRunLoopObserver 0x6080001246a0 [0x101f81df0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce), context = <CFArray 0x60800004cae0 [0x101f81df0]>{type = mutable-small, count = 0, values = ()}}
<CFRunLoopObserver 0x608000124420 [0x101f81df0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce), context = <CFArray 0x60800004cae0 [0x101f81df0]>{type = mutable-small, count = 0, values = ()}}
複製代碼
第一個Observer會監聽RunLoop的進入,它會回調objc_autoreleasePoolPush()向當前的AutoreleasePoolPage增長一個哨兵對象標誌建立自動釋放池。這個Observer的order是-2147483647優先級最高,確保發生在全部回調操做以前。 第二個Observer會監聽RunLoop的進入休眠和即將退出RunLoop兩種狀態,在即將進入休眠時會調用objc_autoreleasePoolPop() 和 objc_autoreleasePoolPush() 根據狀況從最新加入的對象一直往前清理直到遇到哨兵對象。而在即將退出RunLoop時會調用objc_autoreleasePoolPop() 釋放自動自動釋放池內對象。這個Observer的order是2147483647,優先級最低,確保發生在全部回調操做以後。 主線程的其餘操做一般均在這個AutoreleasePool以內(main函數中),以儘量減小內存維護操做(固然你若是須要顯式釋放【例如循環】時能夠本身建立AutoreleasePool不然通常不須要本身建立)。 其實在應用程序啓動後系統還註冊了其餘Observer(例如即將進入休眠時執行註冊回調_UIGestureRecognizerUpdateObserver用於手勢處理、回調爲_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv的Observer用於界面實時繪製更新)和多個Source1(例如context爲CFMachPort的Source1用於接收硬件事件響應進而分發到應用程序一直到UIEvent)。
在主線程執行的代碼,一般是寫在諸如事件回調、Timer回調內的。這些回調會被 RunLoop 建立好的 AutoreleasePool 環繞着,因此不會出現內存泄漏,開發者也沒必要顯示建立 Pool 了。
自動釋放池的建立和釋放,銷燬的時機以下所示
蘋果註冊了一個 Source1 (基於 mach port 的) 用來接收系統事件,其回調函數爲 __IOHIDEventSystemClientQueueCallback()。
當一個硬件事件(觸摸/鎖屏/搖晃等)發生後,首先由 IOKit.framework 生成一個 IOHIDEvent 事件並由 SpringBoard 接收。這個過程的詳細狀況能夠參考這裏。SpringBoard 只接收按鍵(鎖屏/靜音等),觸摸,加速,接近傳感器等幾種 Event,隨後用 mach port 轉發給須要的App進程。隨後蘋果註冊的那個 Source1 就會觸發回調,並調用 _UIApplicationHandleEventQueue() 進行應用內部的分發。
_UIApplicationHandleEventQueue() 會把 IOHIDEvent 處理幷包裝成 UIEvent 進行處理或分發,其中包括識別 UIGesture/處理屏幕旋轉/發送給 UIWindow 等。一般事件好比 UIButton 點擊、touchesBegin/Move/End/Cancel 事件都是在這個回調中完成的。
當上面的 _UIApplicationHandleEventQueue() 識別了一個手勢時,其首先會調用 Cancel 將當前的 touchesBegin/Move/End 系列回調打斷。隨後系統將對應的 UIGestureRecognizer 標記爲待處理。
蘋果註冊了一個 Observer 監測 BeforeWaiting (Loop即將進入休眠) 事件,這個Observer的回調函數是 _UIGestureRecognizerUpdateObserver(),其內部會獲取全部剛被標記爲待處理的 GestureRecognizer,並執行GestureRecognizer的回調。
當有 UIGestureRecognizer 的變化(建立/銷燬/狀態改變)時,這個回調都會進行相應處理。
若是打印App啓動以後的主線程RunLoop能夠發現另一個callout爲**_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv**的Observer,這個監聽專門負責UI變化後的更新,好比修改了frame、調整了UI層級(UIView/CALayer)或者手動設置了setNeedsDisplay/setNeedsLayout以後就會將這些操做提交到全局容器。而這個Observer監聽了主線程RunLoop的即將進入休眠和退出狀態,一旦進入這兩種狀態則會遍歷全部的UI更新並提交進行實際繪製更新。
這個函數內部的調用棧大概是這樣的:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
QuartzCore:CA::Transaction::observer_callback:
CA::Transaction::commit();
CA::Context::commit_transaction();
CA::Layer::layout_and_display_if_needed();
CA::Layer::layout_if_needed();
[CALayer layoutSublayers];
[UIView layoutSubviews];
CA::Layer::display_if_needed();
[CALayer display];
[UIView drawRect];
複製代碼
一般狀況下這種方式是完美的,由於除了系統的更新,還能夠利用setNeedsDisplay等方法手動觸發下一次RunLoop運行的更新。可是若是當前正在執行大量的邏輯運算可能UI的更新就會比較卡,所以facebook推出了AsyncDisplayKit來解決這個問題。AsyncDisplayKit實際上是將UI排版和繪製運算儘量放到後臺,將UI的最終更新操做放到主線程(這一步也必須在主線程完成),同時提供一套類UIView或CALayer的相關屬性,儘量保證開發者的開發習慣。這個過程當中AsyncDisplayKit在主線程RunLoop中增長了一個Observer監聽即將進入休眠和退出RunLoop兩種狀態,收到回調時遍歷隊列中的待處理任務一一執行。
一旦啓動NSURLConnection之後就會不斷調用delegate方法接收數據,這樣一個連續的的動做正是基於RunLoop來運行。 一旦NSURLConnection設置了delegate會當即建立一個線程com.apple.NSURLConnectionLoader,同時內部啓動RunLoop並在NSDefaultMode模式下添加4個Source0。其中CFHTTPCookieStorage用於處理cookie ;CFMultiplexerSource負責各類delegate回調並在回調中喚醒delegate內部的RunLoop(一般是主線程)來執行實際操做。 早期版本的AFNetworking庫也是基於NSURLConnection實現,爲了可以在後臺接收delegate回調AFNetworking內部建立了一個空的線程並啓動了RunLoop,當須要使用這個後臺線程執行任務時AFNetworking經過**performSelector: onThread: **將這個任務放到後臺線程的RunLoop中。
當調用 performSelector:onThread: 時,實際上其會建立一個 Timer 加到對應的線程去,一樣的,若是對應線程沒有 RunLoop 該方法也會失效。
在RunLoop的源代碼中能夠看到用到了GCD的相關內容,可是RunLoop自己和GCD並無直接的關係。當調用了dispatch_async(dispatch_get_main_queue(), <#^(void)block#>)時libDispatch會向主線程RunLoop發送消息喚醒RunLoop,RunLoop從消息中獲取block,而且在__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__回調裏執行這個block。不過這個操做僅限於主線程,其餘線程dispatch操做是所有由libDispatch驅動的。
在界面上有一個UIScrollview控件,若是此時還有一個定時器在執行一個事件,你會發現當你滾動Scrollview的時候,定時器會失效。
- (void)viewDidLoad {
[super viewDidLoad];
[self timer1];
[self timer2];
}
//下面兩種添加定時器的方法效果相同,都是在主線程中添加定時器
- (void)timer1 {
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopDefaultModes];
}
- (void)timer2 {
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
}
複製代碼
由於當你滾動Scrollview的時候,RunLoop會切換到UITrackingRunLoopMode 模式,而定時器運行在defaultMode下面,系統一次只能處理一種模式的RunLoop,因此致使defaultMode下的定時器失效。
解決方法:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
複製代碼
// 得到隊列
dispatch_queue_t queue = dispatch_get_main_queue();
// 建立一個定時器(dispatch_source_t本質仍是個OC對象)
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 設置定時器的各類屬性(幾時開始任務,每隔多長時間執行一次)
// GCD的時間參數,通常是納秒(1秒 == 10的9次方納秒)
// 比當前時間晚1秒開始執行
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
//每隔一秒執行一次
uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
dispatch_source_set_timer(self.timer, start, interval, 0);
// 設置回調
dispatch_source_set_event_handler(self.timer, ^{
NSLog(@"------------%@", [NSThread currentThread]);
});
// 啓動定時器
dispatch_resume(self.timer);
複製代碼
因爲圖片渲染到屏幕須要消耗較多資源,爲了提升用戶體驗,當用戶滾動Tableview的時候,只在後臺下載圖片,可是不顯示圖片,當用戶停下來的時候才顯示圖片。
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"imgName"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];
複製代碼
上面的代碼能夠達到以下效果: 用戶點擊屏幕,在主線程中,三秒以後顯示圖片,可是當用戶點擊屏幕以後,若是此時用戶又開始滾動textview,那麼就算過了三秒,圖片也不會顯示出來,當用戶中止了滾動,纔會顯示圖片。 這是由於限定了方法setImage只能在NSDefaultRunLoopMode 模式下使用。而滾動textview的時候,程序運行在tracking模式下面,因此方法setImage不會執行。
須要建立一個在後臺一直存在的程序,來作一些須要頻繁處理的任務。好比檢測網絡狀態等。
默認狀況一個線程建立出來,運行完要作的事情,線程就會消亡。而程序啓動的時候,就建立的主線程已經加入到RunLoop,因此主線程不會消亡。
這個時候咱們就須要把本身建立的線程加到RunLoop中來,就能夠實現線程常駐後臺。
- (void)viewDidLoad {
[super viewDidLoad];
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[self.thread start];
}
- (void)run
{
NSLog(@"----------run----%@", [NSThread currentThread]);
@autoreleasepool{
/*若是不加這句,會發現runloop建立出來就掛了,由於runloop若是沒有CFRunLoopSourceRef事件源輸入或者定時器,就會立馬消亡。
下面的方法給runloop添加一個NSport,就是添加一個事件源,也能夠添加一個定時器,或者observer,讓runloop不會掛掉*/
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
// 方法1 ,2,3實現的效果相同,讓runloop無限期運行下去
[[NSRunLoop currentRunLoop] run];
}
// 方法2
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
// 方法3
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
NSLog(@"---------");
}
- (void)test
{
NSLog(@"----------test----%@", [NSThread currentThread]);
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
複製代碼
- (void)viewDidLoad {
[super viewDidLoad];
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[self.thread start];
}
- (void)run
{
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] run];
}
複製代碼
若是沒有實現添加NSPort或者NSTimer,會發現執行完run方法,線程就會消亡,後續再執行touchbegan方法無效。
咱們必須保證線程不消亡,才能夠在後臺接受時間處理
RunLoop 啓動前內部必需要有至少一個 Timer/Observer/Source,因此在 [runLoop run] 以前先建立了一個新的 NSMachPort 添加進去了。一般狀況下,調用者須要持有這個 NSMachPort (mach_port) 並在外部線程經過這個 port 發送消息到 RunLoop 內;但此處添加 port 只是爲了讓 RunLoop 不至於退出,並無用於實際的發送消息。
能夠發現執行完了run方法,這個時候再點擊屏幕,能夠不斷執行test方法,由於線程self.thread一直常駐後臺,等待事件加入其中,而後執行。
假設咱們想實現cell的高度緩存計算,由於「計算cell的預緩存高度」的任務須要在最無感知的時刻進行,因此應該同時知足:
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFStringRef runLoopMode = kCFRunLoopDefaultMode;
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler
(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity _) {
// TODO here
});
CFRunLoopAddObserver(runLoop, observer, runLoopMode);
在其中的 TODO 位置,就能夠開始任務的收集和分發了,固然,不能忘記適時的移除這個 observer
複製代碼
歡迎關注公衆號:jackyshan,技術乾貨首發微信,第一時間推送。