前言
RunLoop的初期學(xué)習(xí)總結(jié)袁铐,后續(xù)會持續(xù)研究更新学密。
一淘衙、Runloop定義及作用
1. 什么是Runloop?
RunLoop:顧名思義腻暮,消息運行循環(huán)彤守。首先我們看下蘋果API解釋:
The RunLoop class declares the programmatic interface to objects that manage input sources. An RunLoop object processes input for sources such as mouse and keyboard events from the window system, Port objects, and NSConnection objects. An RunLoop object also processes Timer events.
中文:RunLoop類是用來管理輸入源的編程接口對象。RunLoop對象輸入源來自桌面系統(tǒng)的鼠標(biāo)和鍵盤事件哭靖,端口對象具垫,NSConnection對象。一個RunLoop對象也用來處理計時器事件试幽。
RunLoop和線程關(guān)系如下圖筝蚕,Input sources
和Timer sources
接受事件,然后通知線程進(jìn)行處理铺坞。
- 每個線程都有一個RunLoop起宽,主線程的RunLoop會在App運行的時自動運行,子線程需要手動獲取運行康震,第一次獲取時,才會去創(chuàng)建宾濒。
- 每個RunLoop都會以一個模式mode來運行腿短,可以使用NSRunLoop的方法運行在某個特定的mode。
2. Runloop的定義
Runloop實際上就是一個do...while
循環(huán)绘梦,有任務(wù)時開始橘忱,無任務(wù)時休眠:
3. Runloop的作用
- 保持程序的持續(xù)運行
- 處理APP中的各種事件(觸摸、定時器卸奉、performSelector)
- 節(jié)省cpu資源钝诚、提供程序的性能:該做事就做事,該休息就休息
二榄棵、Runloop Mode
1. Runloop Mode是什么
一個RunLoop包含了多個Mode凝颇,每個Mode又包含了若干個Source、Timer疹鳄、Observer拧略。每次調(diào)用 RunLoop的主函數(shù)時,只能指定其中一個Mode瘪弓,這個Mode被稱作CurrentMode垫蛆。
RunLoop只會運行在一個模式下。想要切換模式,就要暫停當(dāng)前模式袱饭,重新啟動一個運行模式川无。
2. Runloop Mode的五個Mode
系統(tǒng)默認(rèn)注冊了5個Mode:
- kCFRunLoopDefaultMode:App的默認(rèn) Mode,通常主線程是在這個 Mode 下運行的虑乖。
- UITrackingRunLoopMode:界面跟蹤 Mode懦趋,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響决左。
- UIInitializationRunLoopMode:在剛啟動 App 時第進(jìn)入的第一個 Mode愕够,啟動完成后就不再使用。
- GSEventReceiveRunLoopMode:接受系統(tǒng)事件的內(nèi)部 Mode佛猛,通常用不到惑芭。
- kCFRunLoopCommonModes:這是一個占位的 Mode,沒有實際作用继找。
主線程RunLoop有兩個預(yù)置Model:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode遂跟。
3. CommonModes
官方解釋:
commonModes
Objects added to a run loop using this value as the mode are monitored by all run loop modes that have been declared as a member of the set of “common” modes with CFRunLoopAddCommonMode(_: _:)
- kCFRunLoopCommonModes是一個特別的模式,它是一個偽模式婴渡,可以在標(biāo)記為CommonModes的模式下運行幻锁。
- 一個 Mode 可以將自己標(biāo)記為 Common 屬性(通過將其 ModeName 添加到 RunLoop 的 commonModes 中)。每當(dāng) RunLoop 的內(nèi)容發(fā)生變化時边臼,RunLoop 都會自動將 _commonModeItems里的 Source哄尔、Observer、Timer 同步到具有 Common 標(biāo)記的所有 Mode 里柠并。
以tableViewCell中加入timer場景為例:
當(dāng)創(chuàng)建一個 Timer 加到 DefaultMode 時岭接,Timer 會一直得到回調(diào),但如果此時滑動TableView臼予,RunLoop 會將 mode 切換為 TrackingRunLoopMode鸣戴,此時timer停止回調(diào),因為同時只有一種Mode存在粘拾,當(dāng)停止滑動窄锅,mode切換回kCFRunLoopDefaultMode,timer又重新開始回調(diào)缰雇,但此時的timer回調(diào)已經(jīng)是被延遲的錯誤時間的回調(diào)入偷。
如果想讓tableView滑動時timer可以正常調(diào)用,一是手動將這個 timer 分別加入這兩個 Mode械哟,二是將 Timer 加入到CommonModes 里盯串。
NSLog(@"Current AllModes == %@", CFRunLoopCopyAllModes(CFRunLoopGetCurrent()));
NSLog(@"Current Mode == %@", CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent()));
NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"Current AllModes -- %@", CFRunLoopCopyAllModes(CFRunLoopGetCurrent()));
NSLog(@"Current Mode -- %@", CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent()));
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
/** 打印
Current AllModes == (
UITrackingRunLoopMode,
GSEventReceiveRunLoopMode,
kCFRunLoopDefaultMode
)
Current Mode == kCFRunLoopDefaultMode
Current AllModes -- (
UITrackingRunLoopMode,
GSEventReceiveRunLoopMode,
kCFRunLoopDefaultMode,
kCFRunLoopCommonModes
)
Current Mode -- kCFRunLoopDefaultMode
*/
- current mode:runloop當(dāng)前運行模式。
- common modes:存儲的被標(biāo)記為common modes的模式戒良。
打印CFRunLoopGetCurrent()會獲取更多信息体捏,后續(xù)進(jìn)行深入研究。
三、Runloop底層原理
1. Runloop的item — 六大事件
-
block應(yīng)用:
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
-
調(diào)用timer:
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
-
響應(yīng)source0:
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
-
響應(yīng)source1(端口信息):
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
-
GCD主隊列:
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
-
observer源:
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
source0:執(zhí)行performSelectors方法几缭,假如你在主線程performSelectors一個任務(wù)到子線程河泳,這時候就是在代碼中發(fā)送事件到子線程的runloop,這時候如果子線程開啟了runloop就會執(zhí)行該任務(wù)年栓,注意該performSelector方法只有在你子線程開啟runloop才能執(zhí)行拆挥,如果你沒有在子線程中開啟runloop,那么該操作會無法執(zhí)行并崩潰某抓。一個selector執(zhí)行完后會自動從run loop里面移除纸兔,如UIEvent(Touch事件等),在一次觸發(fā)之后就會被runloop移除否副。
source1: 蘋果創(chuàng)建用來接受系統(tǒng)發(fā)出事件汉矿,當(dāng)手機發(fā)生一個觸摸,搖晃或鎖屏等系統(tǒng)备禀,這時候系統(tǒng)會發(fā)送一個事件到app進(jìn)程(進(jìn)程通信)洲拇,這也就是為什么叫基于port傳遞source1的原因,port就是進(jìn)程端口嘛曲尸,該事件可以激活進(jìn)程里線程的runloop赋续,比如你點擊一下app的按鈕或屏幕,runloop就醒過來處理觸摸事件另患。
2. 從堆棧信息探索源碼調(diào)用
通過 bt 命令打印出堆棧信息纽乱,我們可以看到來自于CoreFoundation
中的 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
其它item同理可自行驗證:
查看CFRunloop.c中的源碼
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__() __attribute__((noinline));
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(CFRunLoopTimerCallBack func, CFRunLoopTimerRef timer, void *info) {
if (func) {
func(timer, info);
}
asm __volatile__(""); // thwart tail-call optimization
}
我們可以看到,源碼中先調(diào)起__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
之后才會調(diào)用 func 調(diào)用 timer 昆箕,所以又一次可以驗證鸦列,為什么 timer 需要加到 Runloop 中去了。
3. 通過主線程探究Runloop內(nèi)部機制
// 主線程循環(huán)
CFRunLoopRef mainRunloop = CFRunLoopGetMain();
// CFRunLoop.c
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;
}
// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np(); // 默認(rèn)給主線程
}
__CFSpinLock(&loopsLock);
if (!__CFRunLoops) {
__CFSpinUnlock(&loopsLock);
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
// 主線程为严、mainLoop敛熬、通過dict進(jìn)行綁定:dict[@"pthread_main_thread_np"] = mainLoop
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFSpinLock(&loopsLock);
}
// 其它線程與runloop綁定
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFSpinUnlock(&loopsLock);
if (!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFSpinLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
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)) {
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}
創(chuàng)建RunLoop
/*
_CFRuntimeCreateInstance 這個創(chuàng)建runloop實例方法是關(guān)鍵,
但是看不到內(nèi)部方法肺稀,所以我們主要探究RunLoop對象結(jié)構(gòu)關(guān)系第股。
*/
static CFRunLoopRef __CFRunLoopCreate(pthread_t t) {
CFRunLoopRef loop = NULL;
CFRunLoopModeRef rlm;
uint32_t size = sizeof(struct __CFRunLoop) - sizeof(CFRuntimeBase);
loop = (CFRunLoopRef)_CFRuntimeCreateInstance(kCFAllocatorSystemDefault, __kCFRunLoopTypeID, size, NULL);
if (NULL == loop) {
return NULL;
}
(void)__CFRunLoopPushPerRunData(loop);
__CFRunLoopLockInit(&loop->_lock);
loop->_wakeUpPort = __CFPortAllocate();
if (CFPORT_NULL == loop->_wakeUpPort) HALT;
__CFRunLoopSetIgnoreWakeUps(loop);
loop->_commonModes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
CFSetAddValue(loop->_commonModes, kCFRunLoopDefaultMode);
loop->_commonModeItems = NULL;
loop->_currentMode = NULL;
loop->_modes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
loop->_blocks_head = NULL;
loop->_blocks_tail = NULL;
loop->_counterpart = NULL;
loop->_pthread = t;
#if DEPLOYMENT_TARGET_WINDOWS
loop->_winthread = GetCurrentThreadId();
#else
loop->_winthread = 0;
#endif
rlm = __CFRunLoopFindMode(loop, kCFRunLoopDefaultMode, true);
if (NULL != rlm) __CFRunLoopModeUnlock(rlm);
return loop;
}
繼續(xù)找CFRunLoopRef也就是RunLoop結(jié)構(gòu)體組成:
typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef;
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread; // 線程
uint32_t _winthread;
CFMutableSetRef _commonModes; // Mode集合
CFMutableSetRef _commonModeItems; // ModeItem集合
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
找到CFRunLoopModeRef也就是RunLoopMode結(jié)構(gòu)體組成:
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name;
Boolean _stopped;
char _padding[3];
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
CFMutableDictionaryRef _portToV1SourceMap;
__CFPortSet _portSet;
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 */
};
Runloop機制關(guān)系圖
通過上面源碼追蹤,我們可以總結(jié)如下關(guān)系:
1.Runloop
和線程
是一對一的關(guān)系
2.Runloop
和RunloopMode
是一對多的關(guān)系
3.RunloopMode
和RunloopSource
是一對多的關(guān)系
4.RunloopMode
和RunloopTimer
是一對多的關(guān)系
5.RunloopMode
和RunloopObserver
是一對多的關(guān)系
四. 分析runloop addTimer
通過堆棧找到關(guān)鍵函數(shù)__CFRunLoopDoTimers话原、__CFRunLoopDoTimer夕吻,然后進(jìn)行源碼分析。
源碼分析:
static Boolean __CFRunLoopDoTimers(CFRunLoopRef rl, CFRunLoopModeRef rlm, uint64_t limitTSR) { /* DOES CALLOUT */
Boolean timerHandled = false;
CFMutableArrayRef timers = NULL;
for (CFIndex idx = 0, cnt = rlm->_timers ? CFArrayGetCount(rlm->_timers) : 0; idx < cnt; idx++) {
CFRunLoopTimerRef rlt = (CFRunLoopTimerRef)CFArrayGetValueAtIndex(rlm->_timers, idx);
if (__CFIsValid(rlt) && !__CFRunLoopTimerIsFiring(rlt)) {
if (rlt->_fireTSR <= limitTSR) {
// 收集timers
if (!timers) timers = CFArrayCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeArrayCallBacks);
CFArrayAppendValue(timers, rlt);
}
}
}
// 遍歷所有timers繁仁,然后__CFRunLoopDoTimer涉馅,DoTimer結(jié)束后給標(biāo)記did,然后release釋放黄虱。
for (CFIndex idx = 0, cnt = timers ? CFArrayGetCount(timers) : 0; idx < cnt; idx++) {
CFRunLoopTimerRef rlt = (CFRunLoopTimerRef)CFArrayGetValueAtIndex(timers, idx);
Boolean did = __CFRunLoopDoTimer(rl, rlm, rlt);
timerHandled = timerHandled || did;
}
if (timers) CFRelease(timers);
return timerHandled;
}
// __CFRunLoopDoTimer中會走_(dá)_CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__回調(diào)稚矿,timer block回調(diào)開始執(zhí)行。
static Boolean __CFRunLoopDoTimer(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopTimerRef rlt) { /* DOES CALLOUT */
...省略...
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(rlt->_callout, rlt, context_info);
...省略...
}
根據(jù)堆棧信息追蹤源碼(省略部分源碼),主要對關(guān)鍵部分進(jìn)行分析形成閉環(huán)探索runloop和timer的關(guān)系:[runloop addTimer] -> ...... -> __CFRunLoopDoTimers
-> __CFRunLoopDoTimer
-> __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
-> block回調(diào)晤揣。當(dāng)然Observer桥爽、source也是同理。
RunLoop Mode:
OS下Run Loop的主要運行模式mode有:
- NSDefaultRunLoopMode:默認(rèn)的運行模式昧识,除了NSConnection對象的事件钠四。
- NSRunLoopCommonModes:是一組常用的模式集合,將一個input source關(guān)聯(lián)到這個模式集合上跪楞,等于將input source關(guān)聯(lián)到這個模式集合中的所有模式上缀去。在iOS系統(tǒng)中NSRunLoopCommonModes包含NSDefaultRunLoopMode、NSTaskDeathCheckMode甸祭、UITrackingRunLoopMode缕碎。
- UITrackingRunLoopMode:用于跟蹤觸摸事件觸發(fā)的模式(例如UIScrollView上下滾動), 主線程當(dāng)觸摸事件觸發(fā)會設(shè)置為這個模式淋叶,可以用來在控件事件觸發(fā)過程中設(shè)置Timer阎曹。
- GSEventReceiveRunLoopMode:用于接受系統(tǒng)事件,屬于內(nèi)部的RunLoop模式煞檩。
- 自定義Mode:可以設(shè)置自定義的運行模式Mode处嫌,你也可以用CFRunLoopAddCommonMode添加到NSRUnLoopCommonModes中。
總結(jié)一下:
Run Loop 運行時只能以一種固定的模式運行斟湃,如果我們需要它切換模式熏迹,只有停掉它,再重新開其它凝赛,運行時它只會監(jiān)控這個模式下添加的Timer Source和Input Source注暗,如果這個模式下沒有相應(yīng)的事件源,RunLoop的運行也會立刻返回的墓猎。注意RunLoop不能在運行在NSRunLoopCommonModes模式捆昏,因為NSRunLoopCommonModes其實是個模式集合,而不是一個具體的模式毙沾,我可以添加事件源的時候使用NSRunLoopCommonModes骗卜,只要Run Loop運行在NSRunLoopCommonModes中任何一個模式,這個事件源都可以被觸發(fā)左胞。