runloop是iOS中的基本概念,其他系统也有类似的Event Loop、事件循环,这种关键的基础通常都是基于消息机制。而实现的关键也在于如何在消息未到达的时候,让系统休眠以避免资源占用;消息到达就唤醒系统来执行相应的任务。
Run loops are part of the fundamental infrastructure associated with threads. A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.
本文对iOS中的runloop进行了深入解析,从源码角度对其进行了详细分析,并结合使用场景总结了runloop的常见使用姿势。
runloop是通过 内部维护的事件循环,对事件和消息进行管理 的一个对象。
这里主要是操作系统两种状态的切换:用户态 <===> 内核态。无事件/消息时,休眠以避免资源占用;有事件/消息时,立刻被唤醒处理事件。
CFRunLoopRef是CoreFouncation中的C API,提供了runloop相关的相当丰富的接口,且都是线程安全的。NSRunLoop是对CFRunLoopRef的封装,提供了面向对象的API,非线程安全。
runloop对象会保存在一个全局的容器中,key是线程对象,value是runloop对象。runloop与thread是一一对应的,每个thread的runloop在需要的时候会自动创建,线程中调用currentRunloop相关接口([NSRunLoop currentRunLoop])即创建并开启runloop。而主线程在App的main入口中即开启了runloop,子线程默认不会创建runloop。在线程结束的时候,其对应的runloop(如果有的话)也会销毁。
pthread(基于C的线程底层接口)与NSThread(提供面向对象的API)是对应的,是mach thread的上层封装。
Thread-Local Storage:每个线程都维护了一个存储键值对的字段,记录一些贯穿于线程执行过程中的信息,如runloop对象就存储于其中。
A run loop mode is a collection of input sources and timers to be monitored and a collection of run loop observers to be notified. You must be sure to add one or more input sources, timers, or run-loop observers to any modes you create for them to be useful. You use modes to filter out events from unwanted sources during a particular pass through your run loop.
一个线程有一个runloop,一个runloop包含多个Mode,一个Mode又包含多个Source、Timer、Observer。runloop不能自行切换Mode,只能退出重新指定Mode再运行。
一个Mode可以将自己标记为Common属性(通过将其ModeName添加到runloop的_commonModes集合中即可)。每当runloop的内容变化,runloop会将_commonModeItems集合中的Source/Timer/Observer同步到具有Common属性的所有Mode中。
常见的Mode是DefaultMode和UITrackingMode。除此之外,还有
Mode相关的接口有两个:CFRunLoopAddCommonMode用于添加Mode为Common,CFRunLoopRunInMode使得runloop在指定Mode中运行。
一个runloop的Mode下边包含多个Mode Item,Mode Item的种类有Source、Timer、Observer。一个Mode Item也可以被同时加入到多个runloopMode中,即二者是多对多的关系。所以,一个Mode Item(如NSTimer),可以加到CommonModes(是一个集合,存储了多个Mode的名称,主线程中是DefaultMode和UITrackingMode)中。
runloop的存活必须要伴随着Mode Item的存在,若该Mode中没有了任何Mode Item,则runloop会自动退出。创建一个NSTimer、NSPort、Source,即创建了一个runloopMode item。
Source0用于接收用户事件,如UIEvent等。Source0有公开的API可以使用,开发者可以通过CFRunLoopSourceCreate函数自行创建一个Source0。UIEvent是Source0事件这一点,也可以通过UI事件的断点堆栈信息来看出来:
而本文也会从runloop的源码来分析。
再结合前面RunLoop核心运行流程可以看出Source0(负责App内部事件,由App负责管理触发,例如UITouch事件)和Timer(又叫Timer Source,基于时间的触发器,上层对应NSTimer)是两个不同的Runloop事件源(当然Source0是Input Source中的一类,Input Source还包括Custom Input Source,由其他线程手动发出),RunLoop被这些事件唤醒之后就会处理并调用事件处理方法(CFRunLoopTimerRef的回调指针和CFRunLoopSourceRef均包含对应的回调指针)。
Source1是操作系统内核使用的,而开发者不能使用。Source1是基于mach_msg的原理实现的,必须通过读取某个mach port上的mach内核的消息队列中的消息(mach msg)来触发相应的回调任务。通常用于监听系统端口,与其他线程进行消息通信,能够主动唤醒当前线程的runloop。
Source1在处理任务的时候,通常会跟Source0一起配合,即分发一些任务给Source0去执行。如UITouch事件,最初是由Source1处理点击屏幕到事件捕获的任务,之后Source1将事件包装分发给Source0去处理。这一点非常关键。
NSTimer实际上是基于runloop实现的,其特性受到runloop的极大影响,如NSTimer相关中的持有关系如下:
Current RunLoop -> CFRunLoopMode -> sources数组 -> __NSCFTimer -> _NSTimerBlockTarget -> target 复制代码
runloop对象有六个可监控的状态(时间点)。开发者可以通过对这些状态添加观察者,在这些状态到来时触发一些回调任务,runloop的大部分使用实际上都体现在这里。
我们最常使用的接口应该是runMode:beforeDate:,而推荐做法如下,使用一个全局的变量来控制runloop的运行状态。
BOOL shouldKeepRunning = YES; // global NSRunLoop *theRL = [NSRunLoop currentRunLoop]; while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]); 复制代码
最好是再添加一个@autoreleasepool,以达到更高效的内存管理。
runloop相关的底层数据结构有CFRunLoopRef、CFRunLoopModeRef、CFRunLoopSourceRef、CFRunLoopObserverRef等。
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; }; 复制代码
这里有一些跟开发者日常使用比较密切的关键字段:
关于runloop的mode相关的字段,这里已经非常直观了。_modes、_commonModes、_commonModeItems都是容器而已,这也验证了之前讲到的:一个runloop对应多个mode,一个mode下可以有多个item。而如果在runloop运行过程中想切换mode,则只能退出runloop,再重新指定一个mode进入。这样做可以使得不同mode下的Item相互隔离,不会互相影响。
除此之外,还有一些字段对于理解runloop原理很有帮助:
我们通常使用[NSRunLoop currentRunLoop]或者CFRunLoopGetCurrent()来获取当前线程的runloop,没有则会自动创建。
/// 获取主线程的runloop CFRunLoopRef CFRunLoopGetMain(void) { CHECK_FOR_FORK(); /// 用static变量存储 static CFRunLoopRef __main = NULL; // no retain needed if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed return __main; } /// 获取当前线程的runloop CFRunLoopRef CFRunLoopGetCurrent(void) { CHECK_FOR_FORK(); /// 也是类似全局存储线程对应runloop对象的地方。 CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop); if (rl) return rl; return _CFRunLoopGet0(pthread_self()); } 复制代码
对于主线程的runloop对象,使用static变量__main来存储;对于子线程的runloop对象,也会有对应全局存储的地方(注意这里的TSD)。
_CFRunLoopGet0函数,可以通过当前线程pthread_t对象来获取其runloop对象,如果没有则新创建一个runloop对象。创建之后,将runloop对象保存在__CFRunLoops容器中,同时还会保存在当前线程的TSD中。
/// 全局的Dictionary, key是pthread_t, value是CFRunLoopRef。 static CFMutableDictionaryRef __CFRunLoops = NULL; /// 访问__CFRunLoops时的锁。 static CFLock_t loopsLock = CFLockInit; // should only be called by Foundation // t==0 is a synonym for "main thread" that always works /// 根据pthread来获取线程的runloop 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); /// 首次创建该字典__CFRunLoops,即为主线程创建好了一个runloop。 CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np()); CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop); if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) { CFRelease(dict); } /// 这个局部变量mainLoop要release掉。此时,CFDictionary已经retain了该mainLoop对象。 CFRelease(mainLoop); __CFLock(&loopsLock); } /// 从字典__CFRunLoops中获取pthread_t对应的runloop。 CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); __CFUnlock(&loopsLock); if (!loop) { /// 若没有则为该pthread创建一个runloop。 CFRunLoopRef newLoop = __CFRunLoopCreate(t); __CFLock(&loopsLock); /// 这里又来一次字典中是否有目标runloop的判断,因为第一次判断后已经解锁了,可能在多线程的场景下已经创建好了该runloop 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); /// 此时,Dictionary已经retain了该newLoop对象 CFRelease(newLoop); } if (pthread_equal(t, pthread_self())) { /// 这个_CFSetTSD中也会存储runloop对象 _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL); if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) { /// 注册一个回调__CFFinalizeRunLoop,当线程销毁时,顺便销毁其runloop对象。 _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop); } } return loop; } 复制代码
静态变量__CFRunLoops是一个字典,用于存储runloop对象,key为线程对象(pthread_t),value为runloop对象(CFRunLoopRef)。
*OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile )&__CFRunLoops) 将新创建的临时变量dict赋值给到__CFRunLoops,是一个Barrier操作,用于保证安全。
先看一个值得注意的地方:
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(!loop) 判断,也是为了线程安全。因为多线程情况下,可能两个线程都进入了外层if条件。***__CFLock(&loopsLock)*** 操作在同一时间只允许一个线程执行,但并不会取消其执行。如果没有内层的 if(!loop) 判断,可能导致多个线程都执行到 loop = newLoop; 这个赋值操作。所以,这个双层if判断是必不可少的,很多语言中使用单例对象等场景也需要严格考虑这种情况。
这里,也有一个不理解的地方,多线程情况下,依然会重复调用 CFRunLoopRef newLoop = __CFRunLoopCreate(t); 创建多个runloop对象。如果把__CFLock(&loopsLock);操作提前,且将runloop对象的创建放到内层if判断中,改成如下方式使得多线程情况下只创建一次runloop对象,会有什么问题呢?
if (!loop) { __CFLock(&loopsLock); loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); if (!loop) { CFRunLoopRef newLoop = __CFRunLoopCreate(t); CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop); loop = newLoop; CFRelease(newLoop); } // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it __CFUnlock(&loopsLock); } 复制代码
这样改动应该是不行的。这句注释 // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it 也说明了源码的意图。当然,多创建newLoop对象的开销,影响其实并不大。
_CFRunLoopGet0函数中的大部分代码比较容易理解,只是其中有个_CFGetTSD和_CFSetTSD比较陌生,这个是用于存储thread对象的私有数据,后边会有详细介绍。thread对象的runloop对象即在这个数据中存储着。继续看关键的__CFRunLoopCreate函数,该函数仅有一个参数就是pthread_t对象。
static CFRunLoopRef __CFRunLoopCreate(pthread_t t) { CFRunLoopRef loop = NULL; CFRunLoopModeRef rlm; /// 这里为啥要这样计算runloop对象的内存占用大小 uint32_t size = sizeof(struct __CFRunLoop) - sizeof(CFRuntimeBase); /// 说明:runloop对象的创建也是runtime期间创建的。 loop = (CFRunLoopRef)_CFRuntimeCreateInstance(kCFAllocatorSystemDefault, CFRunLoopGetTypeID(), size, NULL); if (NULL == loop) { return NULL; } /// 初始化_per_run_data (void)__CFRunLoopPushPerRunData(loop); __CFRunLoopLockInit(&loop->_lock); loop->_wakeUpPort = __CFPortAllocate(); if (CFPORT_NULL == loop->_wakeUpPort) HALT; __CFRunLoopSetIgnoreWakeUps(loop); /// commonModes确实是一个CFSet容器 loop->_commonModes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks); /// commonModes中默认就包含DefaultMode 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; } 复制代码
uint32_t size = sizeof(struct __CFRunLoop) - sizeof(CFRuntimeBase); 这一句很神秘。。。
loop对象创建好了以后,就是对它的一系列属性填充操作了:_perRunData、_wakeUpPort、_commonModes,_modes等。
CFSetAddValue(loop->_commonModes, kCFRunLoopDefaultMode); 使得runloop对象一经创建,DefaultMode就加上了common标记。
rlm = __CFRunLoopFindMode(loop, kCFRunLoopDefaultMode, true); 对runloop的DefaultMode进行了确认操作。
这里有两行比较有意思的代码:
/// 这里为啥要这样计算runloop对象的内存占用大小 uint32_t size = sizeof(struct __CFRunLoop) - sizeof(CFRuntimeBase); /// 说明:runloop对象的创建也是runtime期间创建的。 loop = (CFRunLoopRef)_CFRuntimeCreateInstance(kCFAllocatorSystemDefault, CFRunLoopGetTypeID(), size, NULL); 复制代码
__CFRuntimeBase是CoreFoundation的最基础结构,包括__CFBoolean、__CFString、__CFDate、__CFURL、__CFMachPort、__CFBundle等底层结构都有这个 CFRuntimeBase _base; 字段。__CFRuntimeBase中存储的信息包括:是否是系统默认分配的内存、runloop observer相关的fireing或repeats、CFTypeID、是否正在销毁deallocing、是否已经销毁dealloced、是否是自定义引用计数的对象、引用计数,这些在runtime源码中有体现。我们暂时只需要知道,runloop相关的各个数据结构都会有该 CFRuntimeBase _base; 字段,且会使用到该_base字段用于一些信息的存储。
/* All CF "instances" start with this structure. Never refer to * these fields directly -- they are for CF's use and may be added * to or removed or change format without warning. Binary * compatibility for uses of this struct is not guaranteed from * release to release. */ #if DEPLOYMENT_RUNTIME_SWIFT typedef struct __attribute__((__aligned__(8))) __CFRuntimeBase { // This matches the isa and retain count storage in Swift uintptr_t _cfisa; uintptr_t _swift_rc; // This is for CF's use, and must match __NSCFType/_CFInfo layout _Atomic(uint64_t) _cfinfoa; } CFRuntimeBase; #define INIT_CFRUNTIME_BASE(...) {0, _CF_CONSTANT_OBJECT_STRONG_RC, 0x0000000000000080ULL} #else typedef struct __CFRuntimeBase { uintptr_t _cfisa; #if TARGET_RT_64_BIT _Atomic(uint64_t) _cfinfoa; #else _Atomic(uint32_t) _cfinfoa; #endif } CFRuntimeBase; #if TARGET_RT_64_BIT #define INIT_CFRUNTIME_BASE(...) {0, 0x0000000000000080ULL} #else #define INIT_CFRUNTIME_BASE(...) {0, 0x00000080UL} #endif #endif 复制代码
_CFRuntimeCreateInstance函数的介绍如下,使用参数1传入的CFAllocatorRef,进行CF实例的创建工作。具体的创建工作很复杂,涉及到该字段中存储的各类信息,感兴趣可以去研究runtime源码。
CF_EXPORT CFTypeRef _CFRuntimeCreateInstance(CFAllocatorRef allocator, CFTypeID typeID, CFIndex extraBytes, unsigned char *category); /* Creates a new CF instance of the class specified by the * given CFTypeID, using the given allocator, and returns it. * If the allocator returns NULL, this function returns NULL. * A CFRuntimeBase structure is initialized at the beginning * of the returned instance. extraBytes is the additional * number of bytes to allocate for the instance (BEYOND that * needed for the CFRuntimeBase). If the specified CFTypeID * is unknown to the CF runtime, this function returns NULL. * No part of the new memory other than base header is * initialized (the extra bytes are not zeroed, for example). * All instances created with this function must be destroyed * only through use of the CFRelease() function -- instances * must not be destroyed by using CFAllocatorDeallocate() * directly, even in the initialization or creation functions * of a class. Pass NULL for the category parameter. 复制代码
_perRunData是runloop的每次迭代(run)的独立数据,添加volatile防止编译器自行进行优化。参考 Volatile变量。
volatile _per_run_data *_perRunData; // reset for runs of the run loop typedef struct _per_run_data { uint32_t a; uint32_t b; uint32_t stopped; uint32_t ignoreWakeUps; } _per_run_data; 复制代码
_per_run_data的自身结构很简单,也就是用来作一系列状态标记的:
/* Bit 0 of the base reserved bits is used for stopped state */ /* Bit 1 of the base reserved bits is used for sleeping state */ /* Bit 2 of the base reserved bits is used for deallocating state */ // 创建runloop对象的_perRunData属性,返回的是旧的(previous)。 CF_INLINE volatile _per_run_data *__CFRunLoopPushPerRunData(CFRunLoopRef rl) { volatile _per_run_data *previous = rl->_perRunData; rl->_perRunData = (volatile _per_run_data *)CFAllocatorAllocate(kCFAllocatorSystemDefault, sizeof(_per_run_data), 0); rl->_perRunData->a = 0x4346524C; rl->_perRunData->b = 0x4346524C; // 'CFRL' rl->_perRunData->stopped = 0x00000000; rl->_perRunData->ignoreWakeUps = 0x00000000; return previous; } // 将旧的(previous)再赋值过来。 CF_INLINE void __CFRunLoopPopPerRunData(CFRunLoopRef rl, volatile _per_run_data *previous) { if (rl->_perRunData) CFAllocatorDeallocate(kCFAllocatorSystemDefault, (void *)rl->_perRunData); rl->_perRunData = previous; } CF_INLINE Boolean __CFRunLoopIsStopped(CFRunLoopRef rl) { return (rl->_perRunData->stopped) ? true : false; } CF_INLINE void __CFRunLoopSetStopped(CFRunLoopRef rl) { rl->_perRunData->stopped = 0x53544F50; // 'STOP' } CF_INLINE void __CFRunLoopUnsetStopped(CFRunLoopRef rl) { rl->_perRunData->stopped = 0x0; } CF_INLINE Boolean __CFRunLoopIsIgnoringWakeUps(CFRunLoopRef rl) { return (rl->_perRunData->ignoreWakeUps) ? true : false; } CF_INLINE void __CFRunLoopSetIgnoreWakeUps(CFRunLoopRef rl) { rl->_perRunData->ignoreWakeUps = 0x57414B45; // 'WAKE' } CF_INLINE void __CFRunLoopUnsetIgnoreWakeUps(CFRunLoopRef rl) { rl->_perRunData->ignoreWakeUps = 0x0; } 复制代码
这种C语言风格的Push/Pop操作可以说用的非常频繁了。runloop对象创建的时候,就使用 (void)__CFRunLoopPushPerRunData(loop); 进行了_per_run_data的初始化。
runloopMode是一个非常重要的概念,runloop相关的很多数据结构都是以runloopMode进行区分的,即不同runloopMode下的数据都是相互隔离的。
NSMutableSet
切换Mode的方式是否也是push和pop之类的函数名称???
看这个struct CFRunLoopModeRef,其中的 CFRuntimeBase _base; 在创建runloop对象的时候也有使用过。
一个CFRunLoopModeRef对象中,包含了sources0、sources1、observers、timers,以及portSet。每个Mode都有其独有的这些字段,所以同一个runloop的不同Mode的相互隔离是有底层数据结构作为支撑的。
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 */ }; 复制代码
除了 CFRuntimeBase _base; 之外,还有以下这些字段:
如果使用了dispatch source timers相关(MacOS X才有),则会有对应的GCD timerSource和GCD queue等字段。
#if DEPLOYMENT_TARGET_MACOSX #define USE_DISPATCH_SOURCE_FOR_TIMERS 1 #define USE_MK_TIMER_TOO 1 #else #define USE_DISPATCH_SOURCE_FOR_TIMERS 0 #define USE_MK_TIMER_TOO 1 #endif 复制代码
我们通常接触到的runloopMode只有DefaultMode和UITrackingRunLoopMode。实际上,系统还有非常多的其他Mode,一般情况下开发者不会接触到。参考Run loop modes。
__CFRunLoopFindMode()函数根据传入的modeName字符串,找到对应的runloopMode对象,若没有,则会新创建一个。
rlm = (CFRunLoopModeRef)_CFRuntimeCreateInstance(kCFAllocatorSystemDefault, __kCFRunLoopModeTypeID, sizeof(struct __CFRunLoopMode) - sizeof(CFRuntimeBase), NULL); rlm->_name = CFStringCreateCopy(kCFAllocatorSystemDefault, modeName); // 注意这个_portSet。 rlm->_portSet = __CFPortSetAllocate(); // 这个没懂?唤醒runloop时候需要用到的_wakeUpPort。将rl->_wakeUpPort放到rlm->_portSet中。 ret = __CFPortSetInsert(rl->_wakeUpPort, rlm->_portSet); if (KERN_SUCCESS != ret) CRASH("*** Unable to insert wake up port into port set. (%d) ***", ret); #if USE_MK_TIMER_TOO rlm->_timerPort = mk_timer_create(); ret = __CFPortSetInsert(rlm->_timerPort, rlm->_portSet); if (KERN_SUCCESS != ret) CRASH("*** Unable to insert timer port into port set. (%d) ***", ret); #endif 复制代码
rlm = (CFRunLoopModeRef)_CFRuntimeCreateInstance(kCFAllocatorSystemDefault, __kCFRunLoopModeTypeID, sizeof(struct __CFRunLoopMode) - sizeof(CFRuntimeBase), NULL); 也是同样的套路。
初始化_portSet后,会调用 __CFPortSetInsert(rl->_wakeUpPort, rlm->_portSet); 将runloop的_wakeUpPort插入到runloopMode的_portSet中。我们知道,一个runloop可以有多种runloopMode,所以runloop的_wakeUpPort当然要同步到各个runloopMode中去。
另外,有一段使用USE_DISPATCH_SOURCE_FOR_TIMERS包起来的代码,在macOS X上才有的。虽然macOS X上才有,但是看起来好像挺有帮助的?即此时,runloopMode会有一个timer,该timer的回调操作就是将runloopMode的_timerFired字段置为true,即标记为被timer触发。
#if USE_DISPATCH_SOURCE_FOR_TIMERS rlm->_timerFired = false; // 这个queue,后续有用到? rlm->_queue = _dispatch_runloop_root_queue_create_4CF("Run Loop Mode Queue", 0); mach_port_t queuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue); if (queuePort == MACH_PORT_NULL) CRASH("*** Unable to create run loop mode queue port. (%d) ***", -1); rlm->_timerSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, rlm->_queue); __block Boolean *timerFiredPointer = &(rlm->_timerFired); dispatch_source_set_event_handler(rlm->_timerSource, ^{ *timerFiredPointer = true; }); // Set timer to far out there. The unique leeway makes this timer easy to spot in debug output. _dispatch_source_set_runloop_timer_4CF(rlm->_timerSource, DISPATCH_TIME_FOREVER, DISPATCH_TIME_FOREVER, 321); dispatch_resume(rlm->_timerSource); ret = __CFPortSetInsert(queuePort, rlm->_portSet); if (KERN_SUCCESS != ret) CRASH("*** Unable to insert timer port into port set. (%d) ***", ret); #endif 复制代码
RunloopMode Item通常即理解为runloop source、timer、observer。
NSRunLoopCommonModes并非一个真正的RunLoopMode,而是一系列runloopMode的name的集合而已。
commonModes中存的是Mode的名称,一个mode可将自己的name添加到commonModes中,即将自身标记为common的。之后,runloop会将commonModeItems中的Source/Timer/Observer同步到所有打上common标记的Mode中。相当于:分别针对commonModes对应的每一个runloopMode,都添加了commonModeItems中对应的Source。
主线程的commonModes包含两个:Default Mode和UITracking Mode。可通过CFRunLoopAddCommonMode(rl, modeName)来将指定name的mode标记为common。
每个runloop对象都有commonMode相关的两个字段:
CFMutableSetRef _commonModes; CFMutableSetRef _commonModeItems; 复制代码
commonModes是一个 *NSMutableSet<NSString > 的集合。其中存储的是runloopMode的name而已。
也是一个Set集合,包含Observer,Timer,Source。
若runloop中无modeItem则会退出,所以 *** 常驻线程必须有mode item *** 。
items会被同步到所有打上common标记的mode中。
不同mode的items彼此隔离,只能使用commonMode来实现同步。如NSTimer对应可添加到DefaultMode和UITrackingMode。
Runloop Source区分Source0与Source1。
struct __CFRunLoopSource { CFRuntimeBase _base; uint32_t _bits; pthread_mutex_t _lock; CFIndex _order; /* immutable */ CFMutableBagRef _runLoops; union { CFRunLoopSourceContext version0; /* immutable, except invalidation */ CFRunLoopSourceContext1 version1; /* immutable, except invalidation */ } _context; }; 复制代码
其中,version0和version1分别对应着source0和source1.
source对象中有一个特殊的字段 ***CFMutableBagRef _runLoops;***,暂时还不清楚是用作什么用途?
Source0只包含一个回调函数(函数指针),不能主动唤醒runloop,需要手动调用 CFRunLoopWakeUp(runloop) 函数来唤醒runloop。必须先将Source0标记为Signal状态(调用CFRunLoopSourceSignal函数),即将这个Source0标记为待处理(_bits字段),然后调用CFRunLoopWakeUp(runloop)函数来唤醒runloop让其处理该事件。
怎么样是手动调用。。。
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, CFRunLoopMode mode); void (*cancel)(void *info, CFRunLoopRef rl, CFRunLoopMode mode); void (*perform)(void *info); } CFRunLoopSourceContext; 复制代码
其中有三个函数指针:
info即为source相关的一些信息。
Source1包含了一个mach port和一个回调函数(函数指针),可以通过mach port和其他线程通过消息(mach msg)传递的方式进行通信。而mach msg能够做到在用户态和内核态之间进行切换,即以这种方式来主动唤醒runloop,一般用于非常底层的使用场景,在iOS系统内部使用非常广泛。
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; 复制代码
perform函数指针即指向runloop被唤醒后将要处理的事情。
Source1为啥没有schedule和cancel函数指针???
getPort函数指针,用于当source被添加到runloop中的时候,从该函数中获取具体的mach_port_t对象.
NSTimer是与runloop息息相关的,CFRunLoopTimerRef与NSTimer是可以toll-free bridged(免费桥转换)的。当timer加到runloop的时候,runloop会注册对应的触发时间点,时间到了,runloop若处于休眠则会被唤醒,执行timer对应的回调函数。
struct __CFRunLoopTimer { CFRuntimeBase _base; uint16_t _bits; // 标记timer的状态 pthread_mutex_t _lock; CFRunLoopRef _runLoop; CFMutableSetRef _rlModes; // 存储的是runloopMode的名称 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中的结构如下:
CFRunLoopTimerCallBack _callout; 的函数原型为:
typedef void (*CFRunLoopTimerCallBack)(CFRunLoopTimerRef timer, void *info); 复制代码
CFRunLoopTimerContext _context 这个字段,可以用于传递参数到timer对象的回调函数中。
typedef struct { CFIndex version; void * info; const void *(*retain)(const void *info); void (*release)(const void *info); CFStringRef (*copyDescription)(const void *info); } CFRunLoopTimerContext; 复制代码
这两个地方的 *void info 即是同一个。
runloop也提供了直接使用CFRunLoopTimer的接口:
CF_EXPORT CFRunLoopTimerRef CFRunLoopTimerCreate(CFAllocatorRef allocator, CFAbsoluteTime fireDate, CFTimeInterval interval, CFOptionFlags flags, CFIndex order, CFRunLoopTimerCallBack callout, CFRunLoopTimerContext *context); #if __BLOCKS__ CF_EXPORT CFRunLoopTimerRef CFRunLoopTimerCreateWithHandler(CFAllocatorRef allocator, CFAbsoluteTime fireDate, CFTimeInterval interval, CFOptionFlags flags, CFIndex order, void (^block) (CFRunLoopTimerRef timer)) API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0)); 复制代码
CFRunLoopTimerCreate()函数返回一个CFRunLoopTimerRef对象,其使用场景如下:
- (void)testRunloopTimer { CFRunLoopTimerContext context = { 0, (__bridge void *)self, &CFRetain, &CFRelease, NULL }; CFRunLoopTimerRef timer = CFRunLoopTimerCreate(NULL, CFAbsoluteTimeGetCurrent(), 1, 0, 0, &RunloopTimerCallBack, &context); CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer, kCFRunLoopCommonModes); } void RunloopTimerCallBack(CFRunLoopTimerRef timer, void *info) { NSLog(@"RunloopTimerCallBack"); /// 方法1,使用static变量存储self对象。 ViewController *self1 = (__bridge ViewController *)ViewControllerSelf; NSLog(@"%@", self1); /// 方法2,使用CFRunLoopTimerContext来传递self对象。 ViewController *self2 = (__bridge ViewController *)info; NSLog(@"%@", self2); } 复制代码
若要使用在更精准的定时场景,可以放弃NSTimer,转而使用GCD timer。关于GCD Timer的使用,可以参考博客 比较一下iOS中的三种定时器。
runloop的生命周期中,其运行状态有六种:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = (1UL << 0), // 即将进入Loop kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Sources,这里是Source0 kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠 kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒 kCFRunLoopExit = (1UL << 7), // 即将退出runloop }; 复制代码
使用Observer即可对runloop的这些状态进行监听,以完成指定任务。尤其是kCFRunLoopBeforeWaiting这个状态,不仅在iOS内部使用的非常多,开发者对其也比较钟爱,因为一旦到了kCFRunLoopBeforeWaiting状态,iOS系统就要从用户态切换至内核态,进入休眠状态了。也就意味着,此时用户任务已经全部完成了,CPU可以空闲了。
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 */ }; 复制代码
Observer中的字段也很直观,_callout和_context就不多说了,跟timer类似。
observer也包含一个回调函数,在监听的runloop状态出现时触发该回调函数。runloop对observer的使用逻辑,基本与timer一致,都需要指定callback函数,然后通过context可传递参数。只要掌握到了这个套路,关于runloop的这一类使用基本不会陌生了。
如下代码,对runloop的BeforeWaiting状态进行了监听:
- (void)addRunloopObserver { CFRunLoopObserverContext context = { 0, (__bridge void *)self, &CFRetain, &CFRelease, NULL }; CFRunLoopObserverRef observer = CFRunLoopObserverCreate(NULL, kCFRunLoopBeforeWaiting, YES, 0, &RunloopObserverCallBack, &context); CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes); } void RunloopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) { NSLog(@"RunloopObserverCallBack"); } 复制代码
注意,Observer会依赖于特定的runloopMode,所以一旦runloop运行于其他mode,则需要的状态就会无法监听到。
Mach是iOS为内核Darwin的核心,提供了处理器调度、进程间通信(IPC)等基础服务。Mach中的进程间通信是以mach msg的方式,在两个mach port之间传递msg来进行的。发送和接收消息都是通过mach_msg函数来实现的。
runloop的Source1也称为Port-based Source,因其依靠mach msg发送到指定的mach port来唤醒runloop的。mach_msg()函数的原型如下:
/* * Routine: mach_msg * Purpose: * Send and/or receive a message. If the message operation * is interrupted, and the user did not request an indication * of that fact, then restart the appropriate parts of the * operation silently (trap version does not restart). */ mach_msg_return_t mach_msg( mach_msg_header_t *msg, mach_msg_option_t option, mach_msg_size_t send_size, mach_msg_size_t rcv_size, mach_port_name_t rcv_name, mach_msg_timeout_t timeout, mach_port_name_t notify ); 复制代码
mach_msg()内部实际上是调用了mach_msg_trap()系统调用。陷阱trap是操作系统层面的基本概念,用于操作系统状态的更改,如触发内核态与用户态的切换操作。trap通常由异常条件触发,如断点、除0操作、无效内存访问等。若程序没有Source的时候,runloop会进入休眠状态,是通过调用runloop源码中的__CFRunLoopServiceMachPort函数来实现,该函数内部即调用了mach_msg相关的函数操作使得系统内核状态发生改变:用户态切换至内核态。
如:App静止时,处于runloop休眠,点击调试暂停,则主线程调用栈是停留在mach_msg_trap()。
mach_msg()函数可以设置timeout参数,如果在timeout到来之前没有读到msg,当前线程的runloop会处于休眠状态。
mach_msg_header_t中存储了msg的基本信息,包括端口信息等。如本地端口msgh_local_port和远程端口msgh_remote_port,msg的传递方向在header中已经非常明确了。
typedef struct { mach_msg_header_t header; mach_msg_body_t body; } mach_msg_base_t; typedef struct { mach_msg_bits_t msgh_bits; mach_msg_size_t msgh_size; mach_port_t msgh_remote_port; mach_port_t msgh_local_port; mach_port_name_t msgh_voucher_port; mach_msg_id_t msgh_id; } mach_msg_header_t; 复制代码
typedef struct{ mach_msg_size_t msgh_descriptor_count; } mach_msg_body_t; 复制代码
Mach Port实际上就是整数,用于标记端口。
typedef __darwin_mach_port_t mach_port_t; typedef __darwin_natural_t __darwin_mach_port_name_t; /* Used by mach */ typedef __darwin_mach_port_name_t __darwin_mach_port_t; /* Used by mach */ typedef unsigned int __darwin_natural_t; 复制代码
关于mach_port_t的一段说明:
/* * mach_port_t - a named port right * * In user-space, "rights" are represented by the name of the * right in the Mach port namespace. Even so, this type is * presented as a unique one to more clearly denote the presence * of a right coming along with the name. * * Often, various rights for a port held in a single name space * will coalesce and are, therefore, be identified by a single name * [this is the case for send and receive rights]. But not * always [send-once rights currently get a unique name for * each right]. * * This definition of mach_port_t is only for user-space. * */ 复制代码
Thread-specific data(TSD)是线程私有的数据,包含TSD的一些函数用于向线程(thread)对象中存储和获取数据。如CFRunLoopGetMain()函数,调用_CFRunLoopGet0(),在其中即利用了TSD接口从thread中得到runloop对象。所以runloop对象不光存在于全局字典中,也存在于线程的TSD中。
关于TSD的详细源码,请参考CFPlatform.c。
// __CFTSDTable typedef struct __CFTSDTable { uint32_t destructorCount; uintptr_t data[CF_TSD_MAX_SLOTS]; tsdDestructor destructors[CF_TSD_MAX_SLOTS]; } __CFTSDTable; // _CFGetTSD CF_EXPORT void *_CFGetTSD(uint32_t slot) { __CFTSDTable *table = __CFTSDGetTable(); if (!table) { return NULL; } uintptr_t *slots = (uintptr_t *)(table->data); return (void *)slots[slot]; } // _CFSetTSD CF_EXPORT void *_CFSetTSD(uint32_t slot, void *newVal, tsdDestructor destructor) { __CFTSDTable *table = __CFTSDGetTable(); if (!table) { return NULL; } void *oldVal = (void *)table->data[slot]; table->data[slot] = (uintptr_t)newVal; table->destructors[slot] = destructor; return oldVal; } 复制代码
__CFTSDTable即为存储私有数据的数据结构,data数组是私有函数,destructors用来保存data元素对应的析构函数。_CFGetTSD()函数与_CFSetTSD()即提供了对__CFTSDTable的操作接口。如之前新创建runloop对象的代码中:
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop); _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL); _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop); 复制代码
*_CFSetTSD(__CFTSDKeyRunLoop, (void )loop, NULL); 即给当前thread的私有数据的第__CFTSDKeyRunLoop个插槽的位置,设置一个runloop对象。thread中存储runloop对象就是这样实现的。
_CFSetTSD()函数的第三个参数即为该线程销毁时的一个析构函数。如 *_CFSetTSD(__CFTSDKeyRunLoopCntr, (void )(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void ()(void ))__CFFinalizeRunLoop); 即给thread的第__CFTSDKeyRunLoopCntr个插槽设置了一个值,线程销毁时会触发 (void ()(void ))__CFFinalizeRunLoop 函数的调用,即 __CFFinalizeRunLoop 函数是该runloop对象的析构函数。PTHREAD_DESTRUCTOR_ITERATIONS 是线程退出时销毁其私有数据TSD的最大次数。所以,线程退出的时候,runloop对象就是这样销毁的。
实际上,runloop源码中有一系列这样的操作:
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL); _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop); _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(data - 1), (void (*)(void *))__CFFinalizeRunLoop); _CFSetTSD(__CFTSDKeyRunLoopCntr, 0, (void (*)(void *))__CFFinalizeRunLoop); _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL); _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL); // If we received a voucher from this mach_msg, then put a copy of the new voucher into TSD. CFMachPortBoost will look in the TSD for the voucher. By using the value in the TSD we tie the CFMachPortBoost to this received mach_msg explicitly without a chance for anything in between the two pieces of code to set the voucher again. voucher_t previousVoucher = _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, (void *)voucherCopy, os_release); // Restore the previous voucher _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, previousVoucher, os_release); 复制代码
在__CFTSDKeyRunLoop这个slot上存储runloop对象,在__CFTSDKeyRunLoopCntr这个slot上存储runloop对象的析构函数__CFFinalizeRunLoop。源码中,将runloop对象与其 引用计数 分开进行存储。
这里有一个有趣的地方:
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL); __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg); _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL); 复制代码
这里的 (void *)6 和 (void *)0 是干啥呢?原来,__CFRunLoopModeIsEmpty()函数和__CFRunLoopRun()函数中,都有使用 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ) 的判断,以得到libdispatchQSafe变量的值。所以,这个__CFTSDKeyIsInGCDMainQ其实仅仅是用于在 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE(msg) 函数执行前后添加一个判断条件,以此完成类似锁操作而已。
Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ))); if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) return false; // represents the libdispatch main queue 复制代码
Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ))); if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) dispatchPort = _dispatch_get_main_queue_port_4CF(); 复制代码
另外,TSD是如何保证线程销毁时,这些析构函数都能正常调用呢?__CFTSDGetTable函数中有这样一句。
pthread_key_init_np(CF_TSD_KEY, __CFTSDFinalize); 复制代码
__CFTSDFinalize()函数即为CF_TSD_KEY对应的析构函数,也就是线程自身实际的销毁函数。它调用的时候,会遍历TSD中的所有slot,将data置为NULL,调用slot对应的析构函数,析构函数的参数为data。
static void __CFTSDFinalize(void *arg) { __CFTSDSetSpecific(arg); if (!arg || arg == CF_TSD_BAD_PTR) { return; } __CFTSDTable *table = (__CFTSDTable *)arg; table->destructorCount++; for (int32_t i = 0; i < CF_TSD_MAX_SLOTS; i++) { if (table->data[i] && table->destructors[i]) { uintptr_t old = table->data[i]; table->data[i] = (uintptr_t)NULL; table->destructors[i]((void *)(old)); } } // On PTHREAD_DESTRUCTOR_ITERATIONS-1 call, destroy our data if (table->destructorCount == PTHREAD_DESTRUCTOR_ITERATIONS - 1) { free(table); __CFTSDSetSpecific(CF_TSD_BAD_PTR); return; } } 复制代码
所以,线程销毁时,runloop对象对应的析构函数__CFFinalizeRunLoop就是这样调用的。
通过以上的一些底层数据结构,我们对runloop的底层实现有了一个大致的了解: