RunLoop
源碼下載地址RunLoop
其實就是一種事務處理的循環,是事件接受、分發的機制的實現,用來不停的調度工做以及處理輸入事件。其本質就是一個 do-while
循環,若是有輸入則執行對應事件,沒有則休息,這裏RunLoop
的do-while
和普通的do-while
循環不同,普通的循環會讓cpu
處於忙等待的狀況,當cpu
跑滿了也就崩潰了。而runloop
的do-while
循環是一種閒等待,也就是不會消耗cpu
因此runloop
具備休眠功能。runloop
運行原理圖: runloop
在主線程runloop
運行原理圖能夠知道二者之間的關係是一一對應的關係RunLoop
本質RunLoop
底層其實就是一個結構體,是一個__CFRunLoop
對象,具體結構以下: 從對象的結構中能夠得出當前運行的 mode
只會有一個,可是每一個 runloop
裏面是存在多個 mode
的同時也存在着多個 item
也就是事務(source、timer、observer
)面試
RunLoop
以及進一步驗證RunLoop
和線程之間的關係runloop
: CFRunLoopGetMain()
發現底層就是調用 _CFRunLoopGet0
方法獲取 runloop
,入參是主線程runloop
: CFRunLoopGetCurrent()
_CFRunLoopGet0
方法獲取 runloop
此時的入參是當前線程runloop
: _CFRunLoopGet0
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFSpinLock(&loopsLock);
if (!__CFRunLoops) {
//若是存儲runloop的字典不存在則建立
//只有第一次進來的時候纔會走到這
__CFSpinUnlock(&loopsLock);
//建立一個臨時字典
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
//建立主線程
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
// dict : key value
//將線程和runloop以key-value的形式存到臨時字典
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
//OSAtomicCompareAndSwapPtrBarrier 將臨時的字典寫入到__CFRunLoops
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
//釋放mainLoop
CFRelease(mainLoop);
__CFSpinLock(&loopsLock);
}
//經過線程也就是key 到__CFRunLoops中尋找runloop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFSpinUnlock(&loopsLock);
if (!loop) {
//若是沒有找到則基於入參的線程建立一個runloop
//這裏只有多是獲取其餘線程的runloop
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFSpinLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
//此時將新建立的runloop存到__CFRunLoops中
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
//賦值操做
loop = newLoop;
}
// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
__CFSpinUnlock(&loopsLock);
//釋放新建立的runloop
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
的字典是否存在也就是__CFRunLoops
是否存在,若是不存在說明是第一次獲取 runloop
此時建立主線程 runloop
而後存儲到 __CFRunLoops
中key
也就是入參的線程在 __CFRunLoops
中查找 runloop
若是找到了給對應的 runloop
註冊一個回調而後返回,若是沒有找到則基於入參的線程建立一個新的 runloop
而後存到 __CFRunLoops
,而後同樣的註冊一個回調而後返回runloop
的存儲使用的是字典結構,線程是 key
,對應的 runloop
是 value
,從這裏進一步的印證了線程和 runloop的關係是一一對應的RunLoop
建立 就是開闢空間建立 runloop
對象並配置對應參數數組
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name;
Boolean _stopped; //mode是否被終止
char _padding[3];
//幾種事件
CFMutableSetRef _sources0; //sources0 集合
CFMutableSetRef _sources1; //sources1 集合
CFMutableArrayRef _observers; //observers 集合
CFMutableArrayRef _timers; //timers 集合
CFMutableDictionaryRef _portToV1SourceMap;
__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
的結構體當中咱們能夠獲得的消息是每一個 mode
中有若干個 source0、source一、timer、observer
和若干個端口因此事務的執行是由 mode
控制的,而上文張分析 runloop
的結構的時候又知道每一個 runloop
當中都含有多個 mode
,可是當前的 mode
只有一個,因此從這裏能夠總結出兩點:markdown
runloop
管理着 mode
runloop
運行的時候都必須指定有且僅有一個 mode
指定對應的事務,若是須要執行其餘 mode
的事務須要切換 mode
從新進入 runloop
runloop
mode
事務、三者的關係以下圖
Mode的類型
從官方文檔中能夠知道 mode
的類型一共五種以下 其中mode在蘋果文檔中說起的有五個,而在iOS
中公開暴露出來的只有 NSDefaultRunLoopMode
和 NSRunLoopCommonModes
。 NSRunLoopCommonModes
其實是一個 Mode
的集合,默認包括 NSDefaultRunLoopMode
和 NSEventTrackingRunLoopMode
(當以模態方式跟蹤事件(例如鼠標拖動循環)時,應將運行循環設置爲此模式。)。可經過 CFRunLoopAddCommonMode
添加到 runloop
運行模式集合中app
Run Loop Source
分爲Source、Observer、Timer
三種,也就是 ModeItem
async
Source
source
又分爲 source0
和 source1
表示能夠喚醒RunLoop
的一些事件,例如用戶點擊了屏幕,就會建立一個RunLoop
source0
表示非系統事件,即用戶自定義的事件,不能自動喚醒 runloop
須要先調用CFRunLoopSourceSignal(source)
將 source
置爲待處理事件,而後再喚醒 runloop
讓其處理這個事件source1
由RunLoop和內核管理,source1帶有mach_port_t,能夠接收內核消息並觸發回調,如下是source1的結構體Observer
主要用於監聽RunLoop的狀態變化,並做出必定響應,主要有如下一些狀態 Timer
就是是定時器,能夠在設定的時間點拋出回調,綜上所述得知
Runloop
經過監控 Source
來決定有沒有任務要作,除此以外,咱們還能夠用 Runloop Observer
來監控 Runloop
自己的狀態。 Runloop Observer
能夠監控上面的 Runloop
事件,具體流程以下圖。 ide
mode
CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef mode)
添加一個 mode
到 runloop
的 commonmodes中 CFStringRef CFRunLoopCopyCurrentMode(CFRunLoopRef rl)
返回當前運行的 mode
的名稱 從這裏也能夠證實當前運行的 mode
是惟一的CFArrayRef CFRunLoopCopyAllModes(CFRunLoopRef rl)
返回 runloop
中全部的 mode
Source
void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef mode)
添加 source0
或者是 source1
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);
if (modeName == kCFRunLoopCommonModes) {
//若是是kCFRunLoopCommonModes
//則將對應的source添加到_commonModeItems中
CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
if (NULL == rl->_commonModeItems) {
//若是沒有則建立一個
rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
}
//
CFSetAddValue(rl->_commonModeItems, rls);
if (NULL != set) {
//若是set 存在則將source添加到每個common-modes中去
//這裏須要解釋一下 應爲kCFRunLoopCommonModes 是Mode的集合 所需須要在每一個mode中都對應添加這個source
//這樣才能體現kCFRunLoopCommonModes的靈活性 無論設置那個mode 只要改mode在kCFRunLoopCommonModes這個
//集合中source就能執行
CFTypeRef context[2] = {rl, rls};
/* add new item to all common-modes */
CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
CFRelease(set);
}
} else {
//經過modeName 獲取 mode
CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true);
if (NULL != rlm && NULL == rlm->_sources0) {
//若是mode存在可是_sources0不存在則初始化_sources0、_sources一、_portToV1SourceMap
rlm->_sources0 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
rlm->_sources1 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
rlm->_portToV1SourceMap = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, NULL);
}
if (NULL != rlm && !CFSetContainsValue(rlm->_sources0, rls) && !CFSetContainsValue(rlm->_sources1, rls)) {
//若是_sources0和_sources1的集合中都不存在source
if (0 == rls->_context.version0.version) {
//若是version == 0 則添加到_sources0
CFSetAddValue(rlm->_sources0, rls);
} else if (1 == rls->_context.version0.version) {
//若是version == 1 則添加到_sources1
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
步驟:
mode
若是是 commonMode
則將 source
添加到 _commonModeItems
中,而且將 source
添加到每個 common-modes
中commonMode
則先經過 mode
名稱獲取對應 mode
mode
的 source0
是否存在,不存在則初始化_sources0、_sources一、_portToV1SourceMap
source
是否已經在 source0
或者是 source1
的集合中 rls->_context.version0.version
是0,則添加到 source0
集合中source1
的集合中而後吧souce
和一個mach_port_t
對應起來void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef mode)
void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) { /* DOES CALLOUT */
CHECK_FOR_FORK();
Boolean doVer0Callout = false, doRLSRelease = false;
__CFRunLoopLock(rl);
//一樣的是須要判斷mode
if (modeName == kCFRunLoopCommonModes) {
if (NULL != rl->_commonModeItems && CFSetContainsValue(rl->_commonModeItems, rls)) {
CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
//_commonModeItems中刪除對應source
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 {
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);
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);
}
複製代碼
理解了添加的操做,刪除的操做就比較簡單了,這裏就不贅述了void CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef mode)
添加Observer
和添加 source
區別就在於 Observer
若是已經添加到其餘 runloop中去了則不能再被添加,從 __CFRunLoopObserver
和__CFRunLoopSource
也能夠看出差別點 定義的時候__CFRunLoopObserver
就是隻能獲取一個runloop
而__CFRunLoopSource
是一個 runloop
的集合void CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef * mode)
CFRunLoopRemoveSource
void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode)
CFRunLoopAddObserver
void CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode)
CFRunLoopRemoveSource
指定模式下運行 runloop
再看CFRunLoopRunSpecific
方法函數
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
__CFRunLoopLock(rl);
//根據modeName找到本次運行的mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
//若是沒找到 || mode中沒有註冊任何事件,則就此中止,不進入循環
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;
int32_t result = kCFRunLoopRunFinished;
// 1.通知observer即將進入runloop
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
//通知observer已退出runloop
if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
__CFRunLoopModeUnlock(currentMode);
__CFRunLoopPopPerRunData(rl, previousPerRun);
rl->_currentMode = previousMode;
__CFRunLoopUnlock(rl);
return result;
}
複製代碼
大體流程以下:oop
runloop
中是否含有 mode
若是沒有則退出mode
中是否含有 itme
沒有則退出mode
置爲當前運行 mode
Observer
進入 runloop
runloop
這裏調用的是 __CFRunLoopRun
方法Observer
已退出 runloop
因爲該源碼太多這裏就是用僞代碼來代替源碼分析
//核心函數
/* rl, rlm are locked on entrance and exit */
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode){
//經過GCD開啓一個定時器,而後開始跑圈
dispatch_source_t timeout_timer = NULL;
...
dispatch_resume(timeout_timer);
int32_t retVal = 0;
//處理事務,即處理items
do {
// 通知 Observers: 即將處理timer事件
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
// 通知 Observers: 即將處理Source事件
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources)
// 處理Blocks
__CFRunLoopDoBlocks(rl, rlm);
// 處理sources0
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
// 處理sources0返回爲YES
if (sourceHandledThisLoop) {
// 處理Blocks
__CFRunLoopDoBlocks(rl, rlm);
}
// 判斷有無故口消息(Source1)
if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) {
// 處理消息
goto handle_msg;
}
// 通知 Observers: 即將進入休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
__CFRunLoopSetSleeping(rl);
// 等待被喚醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
// user callouts now OK again
__CFRunLoopUnsetSleeping(rl);
// 通知 Observers: 被喚醒,結束休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
handle_msg:
if (被timer喚醒) {
// 處理Timers
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
}else if (被GCD喚醒){
// 處理gcd
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}else if (被source1喚醒){
// 被Source1喚醒,處理Source1
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply)
}
// 處理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)) {
retVal = kCFRunLoopRunFinished;//結束
}
}while (0 == retVal);
return retVal;
}
複製代碼
發現和上文中的 runloop
流程圖是一致的
這裏挑一個處理timers源碼做講解其餘大同小異 __CFRunLoopDoTimers
源碼 發現底層就是經過調用 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
方法來執行 timer
的回調 這裏也能夠經過打印堆棧來驗證 相似這種函數一共有6種:性能
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
經過源碼探索也能夠發現 runloop
其實很簡單,它是一個對象,而且和線程一一對應,主線程的 runloop
是自動開啓的,子線程的 runloop
是須要手動開啓的,每個 runloop
中會有多個 mode
, 每個 mode
中又有多個 source
,可是 runloop
每次 run
只能選擇一種 mode
(這也就形成了 timer
和頁面滑動以後的問題下文有詳細講解)。
RunLoop
運行的核心是一個do..while..
循環,遍歷全部須要處理的事件,若是有事件處理就讓線程工做,沒有事件處理則讓線程休眠,同時等待事件到來。
NSTimer
其實就是上文提到的 timer
,底層 runloop
會在每一個時間點都註冊一個事件,到了時間點回調時間,可是 runloop
爲了節省資源不會每次都很準確的回調事件,timer
有個屬性是寬容度 Tolerance
標識了能夠有的多大偏差,這樣的機制就致使了 timer
不是特別準確。並且若是某個時間點錯過了就不會再重複執行
面試題:以+scheduledTimerWithTimeInterval
:的方式觸發的timer
,在滑動頁面上的列表時,timer會暫停回調, 爲何?如何解決?
應爲scheduledTimerWithTimeInterval
添加方式默認的 mode
是 defaultMode
,而頁面滑動時的 mode
是 UITrackingRunLoopMode
,滑動的時候 runloop
會切換 mode
,上文也提到過 runloop
一次只能執行一個 mode
中的事件,因此對應的 timer
就會中止。
**解決方法是:**將 timer
添加到 NSRunLoopCommonModes
中就好
GCD
的線程管理是經過系統來直接管理的。GCD Timer
是經過 dispatch port
給 RunLoop
發送消息,來使 RunLoop
執行相應的 block
,若是所在線程沒有 RunLoop
,那麼 GCD
會臨時建立一個線程去執行 block
,執行完以後再銷燬掉,所以 GCD
的 Timer
是能夠不依賴 RunLoop
的。
至於這兩個 Timer
的準確性問題,若是不在 RunLoop
的線程裏面執行,那麼只能使用 GCD Timer
,因爲 GCD Timer
是基於 MKTimer(mach kernel timer)
,已經很底層了,所以是很準確的。
若是GCD Timer
在 RunLoop
的線程中執行那麼可能出現的問題和 timer
大同小異
面試題:GCD Timer和 NSTimer那個更精確,爲何
這個問題不能單單直接確定的說是 GCD Timer
上文也分析到了若是在有 RunLoop
的線程中精確度和 NSTimer
差很少,具體緣由能夠看上文的 NSTimer
分析
若是線程中沒有 runloop
首選 GCD timer
,它相對於 NSTimer
更加低層而且沒有 runloop
的影響因此更加精確
RunLoop
裏註冊了兩個 Observer
,其回調都是_wrapRunLoopWithAutoreleasePoolHandler()。
Observer
監視的事件是 Entry
(即將進入 Loop
),其回調內會調用 _objc_autoreleasePoolPush()
建立自動釋放池。其 order
是 -2147483647,優先級最高,保證建立釋放池發生在其餘全部回調以前。Observer
監視了兩個事件: BeforeWaiting
(準備進入休眠) 時調用 _objc_autoreleasePoolPop()
和 _objc_autoreleasePoolPush()
釋放舊的池並建立新池;Exit
(即將退出 Loop
) 時調用 _objc_autoreleasePoolPop()
來釋放自動釋放池。這個 Observer
的 order
是 2147483647,優先級最低,保證其釋放池子發生在其餘全部回調以後。總結一下自動釋放池的建立、銷燬時機:
kCFRunLoopEntry
進入runloop以前,建立一個自動釋放池kCFRunLoopBeforeWaiting
休眠以前,銷燬自動釋放池,建立一個新的自動釋放池kCFRunLoopExit
退出runloop以前,銷燬自動釋放池蘋果註冊了一個 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的回調。
在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驅動的。
因爲圖片渲染到屏幕須要消耗較多資源,爲了提升用戶體驗,當用戶滾動Tableview的時候,只在後臺下載圖片,可是不顯示圖片,當用戶停下來的時候才顯示圖片。 [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"imgName"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];
此方法即時此時正在滑動屏幕也不會出現卡頓的狀況,應爲滑動的過程當中,runloop
的 mode
是 NSEventTrackingRunLoopMode
因此滑動的時候會切換 mode
那麼 NSDefaultRunLoopMode
的事件也就中止了, 滑動完成以後纔會切到 NSDefaultRunLoopMode
而後繼續事件,這樣就不會形成卡頓現象
這個就很簡單了其實就是開個線程將 runloop
跑起來,可是單純的跑 runloop
也不行,若是事件執行完了 runloop
就會消亡對應的線程也就會銷燬。因此能夠添加一個 timer
或者是 NSPort
具體方法以下:
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
三種方法都能保證 runloop
運行,線程常駐