在 Objective-C
中,说到RunLoop
咱们总是很难绕过去下面这些问题。
啥是RunLoop
?它与线程间又是何种联系?所谓的 RunLoopMode
、RunLoopSource
、RunLoopObserver
、RunLoopTimer
又是些啥?有啥关联呢?
下面,咱们无妨以这些为点,由点入面,剖析 RunLoop
的真正面貌。
写在最前
在 OSX/iOS
系统中,RunLoop
相关的有两个:NSRunLoop 和 CFRunLoopRef。
- CFRunLoopRef 是在 CoreFoundation 结构内的,它提供了纯 C 函数的 API,并且一切这些 API 都是线程安全的。
- NSRunLoop 是根据 CFRunLoopRef 的封装,提供了面向目标的 API,可是这些 API 不是线程安全的。
现在而言,NSRunLoop 是 Foundation
结构内的一个类,咱们只能看到其头文件,而看不到其详细的实现。而 CFRunLoopRef 是开源的,咱们能够直接下载其源码。直达下载
__CFRunLoop
已然 NSRunLoop是根据CFRunLoopRef的封装,咱们就直接从 CFRunLoopRef 源码下手来看看 RunLoop
究竟是个啥?
咱们能从源码中直接拿到 CFRunLoop
的界说:
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;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
能够看到,其本质也是个结构体,咱们能够只抓住咱们关心的重要内容,比方 _pthread
、_modes
、_currentMode
、_commonModeItems
、_commonModes
等等。
能够确定的是:
-
_pthread
能看出来RunLoop
跟线程必定是有联系的,详细什么联系呢?咱们后边再看。 -
_modes
便是咱们前面说到的RunLoopMode
,这儿是一个set
,很明显不止一种,是多种。 -
_currentMode
呢?直译也能知道是当时的mode
。 -
_commonModeItems
、_commonModes
很明显跟咱们通常项目中设置的commonMode
有联系,而这儿又将其与一般的_modes
区分隔,明显并不是一类形式,详细咱们后边再看。
__CFRunLoopMode
接下来咱们持续来看 __CFRunLoopMode
的界说:
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 */
};
相同是一个结构体,而咱们也相同抓住咱们比较关心的 _sources0
、_sources1
、_observers
、_timers
等等。
留意⚠️,这儿都是复数方法,而看其数据类型也是 set 或许 array,印证其并不是单个的存在。
咱们持续撸源码,很容易发现,__CFRunLoopSource
、__CFRunLoopObserver
、__CFRunLoopTimer
也都是结构体,也都有其对应的特点。
稍作总结
到这儿,其实咱们早年面的源码就能知道 RunLoop
、RunLoopMode
、RunLoopSource
、RunLoopObserver
、RunLoopTimer
他们之前的联系:
-
RunLoop
与RunLoopMode
是一对多的联系,也便是一个RunLoop
包括多种RunLoopMode
,而_currentMode
阐明其多个mode
并不是一起收效的,一次仅仅收效其间的一种; -
RunLoopMode
与RunLoopSource
、RunLoopObserver
、RunLoopTimer
相同也是一对多的联系,换句话说,便是在一种RunLoopMode
下,会有多个RunLoopSource
/RunLoopObserver
/RunLoopTimer
,留意三者并不是互斥联系,一种RunLoopMode
是能够一起拥有上面三种的,当然,RunLoopSource
又分为通常咱们熟知的source0
和source1
。而上面三者又统称为modeItem
;
网上找到一张图来描述他们之前的联系:
这儿面还说到了 RunLoop
与线程,以及commonModes
的联系,后边咱们持续看。
RunLoop 怎么 Run ?
前面咱们知道,RunLoop
其本质是一个结构体,那么,RunLoop
又是怎么运转起来的呢?咱们知道 RunLoop
是一个 Loop
,也便是循环,通常咱们将其理解为 死循环,这又是为何呢?
咱们能够从其主运转函数下手得知一二:
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
能够看到,CFRunLoopRun
其自身实现为一个 do-while
循环体,其完毕的条件是 Stop 或许 Finish 。而这两种状态在正常状况下一直不会满足,所以说理解一个正常的 RunLoop
是一个循环,仍是个 死循环 也是无可厚非的。
另,咱们从上面的源码了解到,其 Run
起来的中心函数是 CFRunLoopRunSpecific()
:
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
__CFRunLoopLock(rl);
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
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);
CFRunLoopModeRef previousMode = rl->_currentMode;
rl->_currentMode = currentMode;
int32_t result = kCFRunLoopRunFinished;
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
__CFRunLoopModeUnlock(currentMode);
__CFRunLoopPopPerRunData(rl, previousPerRun);
rl->_currentMode = previousMode;
__CFRunLoopUnlock(rl);
return result;
}
咱们能够大致了解起进程,这个函数传入参数 RunLoop
、mode名、时刻以及是否 Handled 之后回来的 Boolean 值,其整个进程便是:经过 __CFRunLoopFindMode()
找到 currentMode
,终究经过 __CFRunLoopRun()
运转。
这中心,免不了一堆各种判断,可是不影响主流程。
值得一提的是,咱们放眼望去那些个各种 lock
、unlock
,实践上便是咱们线程安全的保证。
实践上,RunLoop
run起来的姿势还有一种,那便是 CFRunLoopRunInMode()
:
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
从上面的源码知道,其本质仍是 CFRunLoopRunSpecific()
,跟上面是共同的。
从这儿实践上咱们也能发现另一个问题:RunLoopRun
总是 run
在某一种 mode
下面的,也就对应前面说到的 _currentMode
的问题了。换句话说便是,一个 RunLoop
有许多 mode
,可是每次运转仅仅指定其一种。咱们在让其运转的时分能够指定其运转的 mode
,也能够不指定,走默认值,默认值为 kCFRunLoopDefaultMode
。
接着深入一下,已然只能是一个 mode
在运转,那只有这个 mode
下面的 modeItems
收效是不是便是理所应当,这儿便是为什么 Timer 在 kCFRunLoopDefaultMode
下时,滑动会暂停其计时了,由于滑动的时分,mode
切换了。所以,了然与否呢?
RunLoop 与线程的联系
前面,RunLoop
的结构体中,咱们看到一个 RunLoop
中对应一个 _pthread
,即线程,由于这儿并不是相似前面说到的 set
OR Array
的这种调集的数据结构。
那么一个线程中,是否能有多个 RunLoop
呢?其实不然,官方根本就没有给予咱们自己创建 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;
}
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
这儿都调用了 _CFRunLoopGet0()
:
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
if (!__CFRunLoops) {
__CFUnlock(&loopsLock);
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
if (!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&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
__CFUnlock(&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;
}
留意这儿:CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
很明显的是,这儿有个字典(Dictionary),经过这个字典能经过线程的指针,拿到目标应的 RunLoop
,所以呢?要是一个线程对应多个 RunLoop
,这儿怎么解说?
所以,线程和 RunLoop
之间是一一对应的,并且其联系是保存在一个全局的 Dictionary 里。
实践上,线程刚创建时并没有 RunLoop
,如果你不自动获取,那它一直都不会有。RunLoop
的创建是产生在第一次获取时,RunLoop
的毁掉是产生在线程完毕时。你只能在一个线程的内部获取其 RunLoop
(主线程在外)。
commonMode
截止现在,咱们还遗留另一个问题,关于 commonMode
的,这儿又是何解呢?
咱们先来看这段源码:
void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName) {
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return;
__CFRunLoopLock(rl);
if (!CFSetContainsValue(rl->_commonModes, modeName)) {
CFSetRef set = rl->_commonModeItems ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModeItems) : NULL;
CFSetAddValue(rl->_commonModes, modeName);
if (NULL != set) {
CFTypeRef context[2] = {rl, modeName};
/* add all common-modes items to new mode */
CFSetApplyFunction(set, (__CFRunLoopAddItemsToCommonMode), (void *)context);
CFRelease(set);
}
} else {
}
__CFRunLoopUnlock(rl);
}
这个函数是干啥的呢?其实便是给 RunLoop
增加 commonMode
的,便是将一个一般的 mode
标记为 commonMode
(留意这儿 modeName
并不是 kCFRunLoopCommonModes
),咱们能够看到,其终究是增加到了 RunLoop
的 _commonModes
特点里边,可是这儿加进去的仅仅 mode
的 name
。
而在之后呢?留意这个函数 CFSetApplyFunction()
,咱们能够直接看其注释就知道详细干了啥,便是将一切的 commonModeItems
加到这个新的 commonMode
里边了。详细增加的进程是经过 __CFRunLoopAddItemsToCommonMode()
函数增加的:
static void __CFRunLoopAddItemsToCommonMode(const void *value, void *ctx) {
CFTypeRef item = (CFTypeRef)value;
CFRunLoopRef rl = (CFRunLoopRef)(((CFTypeRef *)ctx)[0]);
CFStringRef modeName = (CFStringRef)(((CFTypeRef *)ctx)[1]);
if (CFGetTypeID(item) == CFRunLoopSourceGetTypeID()) {
CFRunLoopAddSource(rl, (CFRunLoopSourceRef)item, modeName);
} else if (CFGetTypeID(item) == CFRunLoopObserverGetTypeID()) {
CFRunLoopAddObserver(rl, (CFRunLoopObserverRef)item, modeName);
} else if (CFGetTypeID(item) == CFRunLoopTimerGetTypeID()) {
CFRunLoopAddTimer(rl, (CFRunLoopTimerRef)item, modeName);
}
}
能够看到,其便是将这个 mode
里边的各类 modeItem
增加到 commonMode
里边。
咱们在接着往下看,拿 CFRunLoopAddSource
来看:
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) {
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) {
CFTypeRef context[2] = {rl, rls};
/* add new item to all common-modes */
CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
CFRelease(set);
}
} else {
CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true);
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);
}
if (NULL != rlm && !CFSetContainsValue(rlm->_sources0, rls) && !CFSetContainsValue(rlm->_sources1, rls)) {
if (0 == rls->_context.version0.version) {
CFSetAddValue(rlm->_sources0, rls);
} 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) {
CFDictionarySetValue(rlm->_portToV1SourceMap, (const void *)(uintptr_t)src_port, rls);
__CFPortSetInsert(src_port, rlm->_portSet);
}
}
__CFRunLoopSourceLock(rls);
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 */
}
}
咱们这儿走的是 modeName != kCFRunLoopCommonModes
的状况(由于这儿 modeName
从一开始就不是 kCFRunLoopCommonModes
,是透传过来的一个正常的 mode
,前面一开始便是要将这个 mode
增加到 commonModes
中),明显就会将 item
同步到 mode
对应的 items
里边了。
⚠️留意,这儿并不是调用一次,是将上面 set
里边的每个元素都调用一次上面的进程。也便是将一切的 commonModeItems
加到了新的 mode
里边了。
咱们再关注一下 modeName == kCFRunLoopCommonModes
的时分(这是另外一种状况了,什么状况呢?比方咱们让 Timer
run 在 commonMode
下的时分),首先是将 source
(item
) 增加到了 RunLoop
的 _commonModeItems
里边。然后呢?
咱们留意到这儿也有相似的调用:
if (NULL != set) {
CFTypeRef context[2] = {rl, rls};
/* add new item to all common-modes */
CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
CFRelease(set);
}
咱们再来看看 __CFRunLoopAddItemToCommonModes
做了什么:
static void __CFRunLoopAddItemToCommonModes(const void *value, void *ctx) {
CFStringRef modeName = (CFStringRef)value;
CFRunLoopRef rl = (CFRunLoopRef)(((CFTypeRef *)ctx)[0]);
CFTypeRef item = (CFTypeRef)(((CFTypeRef *)ctx)[1]);
if (CFGetTypeID(item) == CFRunLoopSourceGetTypeID()) {
CFRunLoopAddSource(rl, (CFRunLoopSourceRef)item, modeName);
} else if (CFGetTypeID(item) == CFRunLoopObserverGetTypeID()) {
CFRunLoopAddObserver(rl, (CFRunLoopObserverRef)item, modeName);
} else if (CFGetTypeID(item) == CFRunLoopTimerGetTypeID()) {
CFRunLoopAddTimer(rl, (CFRunLoopTimerRef)item, modeName);
}
}
乍一看,是不是跟上面的 __CFRunLoopAddItemsToCommonMode
如出一辙? 其实不然,函数名上一个是 item
一个是 items
,仅仅是这个差异么?不是的,看看其获取的参数,value
及经过 ctx
获取的值是不一样的。
换句话说,这儿操作虽然相似,可是调用方由于 set
的不一样(一个是 modes
,一个是 items
),所以这儿虽然调用的也是前面说到的 CFRunLoopAddSource
函数,可是参数不一样了,modeName
这时分便是 kCFRunLoopCommonModes
,走的便是 modeName == kCFRunLoopCommonModes
的逻辑了。
这儿有点绕,搞不好就在这死循环了,但终究厘清之后就会如注释所述,其意图便是将 item
同步到 RunLoop
的特点 _commonModes
中的一切 mode
的对应的 sourceItems
调集中。
总结来看的话,commonMode
其实并不同于寻常的 mode
,它更多的是一个虚的概念(其他的则是实践的 mode
),当是这种形式的时分,若是将 mode
标记为 commonMode
,会将其增加到 RunLoop
的 _commonModes
特点里边,并将 _commonModeItems
同步到这个 mode
中的 sourceItems
中。若是 item
运转在 commonMode
,会先将其增加到 RunLoop
的 _commonModeItems
特点里边,并同步到 _commonModes
特点中的一切 mode
的 sourceItems
中。
一句话总结便是,commonMode
在这儿需求保证一个 modes
与 items
的双向同步。
总结
到这儿,咱们基本厘清了 RunLoop
中的一堆概念及其联系,在此总结一二:
-
RunLoop
与RunLoopMode
是一对多的联系,也便是一个RunLoop
包括多种RunLoopMode
; -
RunLoopMode
与modeItem
(RunLoopSource
、RunLoopObserver
、RunLoopTimer
) 相同也是一对多的联系,换句话说,便是在一种RunLoopMode
下,会有多个modeItem
; -
RunLoop
只能运转在一种RunLoopMode
下,即为currentMode
; -
RunLoop
与线程是一一对应的联系,其联系保存在全局的字典里边(__CFRunLoops
),key
是线程目标的指针,value
便是RunLoop
目标。 -
RunLoop
中的_commonModes
特点中存储的是被标记为commonMode
的一切modes
,仅仅存储了modeName
; -
RunLoop
中的_commonModeItems
特点中存储的是一切运转在commonMode
下面的一切的modeItems
; -
mode
、modeItem
在被标记为commonMode
的进程中,就会同步RunLoop
的两个特点_commonModes
和_commonModeItems
,以及同步一切的commonModes
的modeItems
; - 在
CFRunLoopRef
中触及或许影响线程安全的操作都是加锁的,所以是线程安全的;
以上,期望都能有所收获。