博客链接重拾RunLoop原理
更新于2019.07.26
虽然自己很早前就看过RunLoop的源码,当时看得时候,有点地方还是比较生涩的。所有抽了个时间,重新整理了一下之前RunLoop的笔记。CoreFoundation源代码关于RunLoop的源码主要集中在CFRunLoop.c
文件中。
RunLoop的获取
苹果并不允许我们直接创建RunLoop,RunLoop的创建在第一次获取的时候,使用[NSRunLoop mainRunLoop]
或CFRunLoopGetMain()
可以获取主线程的RunLoop;通过[NSRunLoop currentRunLoop]
或CFRunLoopGetCurrent()
获取当前线程的RunLoop。
它们之间的关系是Foundation
中的RunLoop是对Core Foundation
中的包装。可以通过执行NSLog(@"%@, %p", [NSRunLoop mainRunLoop], CFRunLoopGetMain());
得出,这里就不贴实验结果了。
接着看一下RunLoop在CFRunLoop.c
中的定义:
// 主线程的RunLoop
CFRunLoopRef CFRunLoopGetMain(void) {
CHECK_FOR_FORK(); // 判断是否需要fork进程
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();
// 先从TSD中查找有没有相关的runloop信息,有则返回。
// 我们可以理解为runloop不光存在与全局字典中,也存在中TSD中。
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
CHECK_FOR_FORK
用来判断是否需要fork进程,这里我们可以暂时不管。
在获取主线程RunLoop的时候,它使用了static CFRunLoopRef __main
进行保存,当第二次调用CFRunLoopGetMain()
,__main
是有值的,就不会再重新创建,否则就使用_CFRunLoopGet0
进行创建,传入的是pthread_main_thread_np()
即主线程。
在获取当前线程的RunLoop的时候,首页会通过_CFGetTSD
获取RunLoop,如果没有再通过_CFRunLoopGet0
,传入的是当前的线程。
这里介绍一下Thread-specific data
。Thread-specific data
是线程私有数据就是上面的TSD
,顾名思义就是存一些特定的数据的,RunLoop会保存在线程的私有数据里。
// __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
数组用来保存析构函数,destructorCount
用来记录析构函数的个数。
_CFGetTSD
的作用就是获取__CFTSDTable
的data
数据,并返回slot
对应的值。
_CFSetTSD
的作用就是给__CFTSDTable
里设置data[slot]
和destructors[slot]
位置的值。
RunLoop与线程之间的关系
要想知道RunLoop与线程之间的关系,就需要看一下_CFRunLoopGet0
函数。
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
// 当前线程为0,则取主线程
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
// __CFRunLoops是一个全局的静态字典。
// 如果该字典为空,就进行以下操作:
// 1.创建一个临时字典;
// 2.创建主线程的RunLoop,并将它存到临时字典里
// 3.OSAtomicCompareAndSwapPtrBarrier用来将这个临时字典复制到全局字典里;
// 并且使用了锁机制确保上述操作的安全性。
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);
}
// 当前线程RunLoop的获取,获取不到就使用__CFRunLoopCreate创建一个RunLoop,并保存在全局字典里
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())) {
// t为当前线程的话,将loop保存在线程私有数据中
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
// __CFFinalizeRunLoop是RunLoop的析构函数,
// PTHREAD_DESTRUCTOR_ITERATIONS 表示是线程退出时销毁线程私有数据的最大次数
// 这也是RunLoop的释放时机--线程退出的时候
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
// 注册一个回调,当线程销毁时,顺便也销毁其对应的RunLoop
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}
通过源代码我们可以知道:
- RunLoop和线程之间是一一对应的,它们之间的关系保存在一个全局字典以及线程私有数据中;
- 全局字典以线程为Key,RunLoop对象为Value的形式保存RunLoop和线程之间的映射关系;
- 在线程创建的时候,是没有对应的RunLoop,它的创建是在第一次获取的时候,它的销毁则发生在线程销毁的时候。
之前在看源码的时候有两个地方不是很理解:
1.为什么上面的loop要再取一次
后来在《程序员的自我修养》第29页中得到启发。里面关于单例有这样一段代码:
volatile T* pInst = 0;
T* GetInstance()
{
if(pInst == NULL)
{
lock();
if(pInst == NULL)
pInst = new T;
unlock();
}
return pInst;
}
书上只说明双重if在这里可以让lock的调用开销降到最低。为什么有这个效果,这里做一下说明。
在不考虑CPU乱序的情况下,假设有两个线程A、B同时访问GetInstance()
,A和B同时执行第一个判断语句,结果一样,都进入了代码块。lock()
的设定就是只允许一个线程进入,假设A先进入,B在等待。A进入后首先判断pInst
为NULL
,那么new一个对象,然后解锁返回对象。唤醒B,这是B进入发现第二个判断通过不了(因为pInst
已经有值了),这样的话B就直接解锁返回对象。假设只有最外层的判断的话,那么B也会创建一个对象。
我想这里应该也是类似的作用吧。
2.RunLoop销毁的时机
上面的源代码只说明了这个会在RunLoop的析构函数是__CFFinalizeRunLoop
,但是具体的释放时机会在后面说明。
RunLoop的创建
从_CFRunLoopGet0
函数的实现中可以知道,RunLoop的创建是通过调用使用__CFRunLoopCreate
返回一个CFRunLoopRef
的实例,这个函数大致分为两步:
- 使用
_CFRuntimeCreateInstance
创建一个CFRunLoopRef
实例,其实现为CFRuntime.c
文件; - 对
CFRunLoopRef
进行初始化配置,包括调用__CFRunLoopFindMode(loop, kCFRunLoopDefaultMode, true);
。
另外在__CFRunLoopFindMode
里讲到了RunLoop的定时器,用宏进行了判断
#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
在MACOSX
下,RunLoop会使用GCD Timer
和MK_TIMER
来做定时器,在非MACOSX
下,使用MK_TIMER
作为定时器。
RunLoop的释放
我们知道RunLoop的释放是发生在线程销毁的时候。
在__CFTSDGetTable()
函数的实现中有这样的一句代码:
pthread_key_init_np(CF_TSD_KEY, __CFTSDFinalize);
通过CF_TSD_KEY
,指定了对应的析构函数__CFTSDFinalize
是一个析构函数。
__CFTSDFinalize
的实现如下:
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));
}
}
if (table->destructorCount == PTHREAD_DESTRUCTOR_ITERATIONS - 1) { // On PTHREAD_DESTRUCTOR_ITERATIONS-1 call, destroy our data
free(table);
__CFTSDSetSpecific(CF_TSD_BAD_PTR);
return;
}
}
我们可以看到,table
会循环遍历data
和destructors
的数据,并且把old
变量作为destructors
里函数的参数。table->destructors[i]((void *)(old));
相当于就是在调用一个析构函数。通过前面的代码,我们知道RunLoop的析构函数是会存到destructors
中去的。所以当线程退出的时候,会调用到RunLoop的析构函数__CFFinalizeRunLoop
释放RunLoop。
接着看一下__CFFinalizeRunLoop
函数
// Called for each thread as it exits
CF_PRIVATE void __CFFinalizeRunLoop(uintptr_t data) {
CFRunLoopRef rl = NULL;
if (data <= 1) {
__CFLock(&loopsLock);
if (__CFRunLoops) {
rl = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(pthread_self()));
if (rl) CFRetain(rl);
// 移除全局字典中RunLoop与线程之间的映射关系
CFDictionaryRemoveValue(__CFRunLoops, pthreadPointer(pthread_self()));
}
__CFUnlock(&loopsLock);
} else {
// 递归移除
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(data - 1), (void (*)(void *))__CFFinalizeRunLoop);
}
if (rl && CFRunLoopGetMain() != rl) { // protect against cooperative threads
if (NULL != rl->_counterpart) {
CFRelease(rl->_counterpart);
rl->_counterpart = NULL;
}
// purge all sources before deallocation
CFArrayRef array = CFRunLoopCopyAllModes(rl);
for (CFIndex idx = CFArrayGetCount(array); idx--;) {
CFStringRef modeName = (CFStringRef)CFArrayGetValueAtIndex(array, idx);
// 移除RunLoop中的mode
__CFRunLoopRemoveAllSources(rl, modeName);
}
// 移除RunLoop中的common mode
__CFRunLoopRemoveAllSources(rl, kCFRunLoopCommonModes);
CFRelease(array);
}
if (rl) CFRelease(rl);
}
RunLoop相关的类与作用
在CFRunLoop.c
中关于RunLoop的类一共有五个,它们分别是CFRunLoopRef
、CFRunLoopModeRef
、CFRunLoopSourceRef
、CFRunLoopObserverRef
、CFRunLoopTimerRef
。各个类之间的关系:
CFRunLoopRef
CFRunLoopRef
对应__CFRunLoop
结构体,它的定义如下:
struct __CFRunLoop {
// 省略其他成员变量
...
// common mode的集合
CFMutableSetRef _commonModes;
// 每个common mode都有的item(source,timer and observer)集合
CFMutableSetRef _commonModeItems;
// 当前runloop的mode
CFRunLoopModeRef _currentMode;
// 所有的mode的集合
CFMutableSetRef _modes;
};
一个RunLoop可以包含几个Mode,但是必须指定一个Mode来运行,它取决于_currentMode
的值。关于_currentMode
的赋值在CFRunLoopRunSpecific
函数中。
CFRunLoopModeRef
接着看CFRunLoopModeRef
,CFRunLoopModeRef
对应着__CFRunLoopMode
结构体,其定义如下:
struct __CFRunLoopMode {
CFStringRef _name;
// source0的集合
CFMutableSetRef _sources0;
// source1的集合
CFMutableSetRef _sources1;
// observer的数组
CFMutableArrayRef _observers;
// timer的数组
CFMutableArrayRef _timers;
// 省略其他属性
...
};
__CFRunLoopMode
中包含的就是RunLoop要处理的一些事情(source0/source1/observer/timer)。前面提到RunLoop必须在执行的Mode下运行,如果RunLoop需要切换Mode,只能退出Loop,再重新指定一个Mode进入。这样的好处是:不同组的source0/source1/observer/timer可以相互隔离,互不影响,从而提高执行效率。
RunLoop的Mode
RunLoop有五种运行模式,其中常见的1、2和5这三种
-
kCFRunLoopDefaultMode
:App的默认Mode,通常主线程是在这个Mode下运行; -
UITrackingRunLoopMode
:界面跟踪Mode,用于滚动视图追踪触摸滑动,保证界面滑动时不受其他 Mode影响; -
UIInitializationRunLoopMode
:在刚启动App时第进入的第一个Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultMode; -
GSEventReceiveRunLoopMode
:接受系统事件的内部Mode; -
kCFRunLoopCommonModes
:这是一个占位用的Mode,并不是一种真正的Mode;
CommonModes
kCFRunLoopCommonModes
是苹果提供的一种“CommonModes”。它其实是一个标识符,并不是一个具体的Mode。kCFRunLoopDefaultMode
和UITrackingRunLoopMode
,并且都被标记为“CommonModes”。
一个Mode可以将自己标记为“Common”属性(通过将其ModeName
添加到RunLoop的commonModes
中)。每当RunLoop的内容发生变化时,RunLoop都会自动将_commonModeItems
里的source0/source1/observer/timer同步到具有“Common”标记的所有Mode里,即能在所有具有“Common”标记的所有Mode里运行。
以CFRunLoopAddSource
函数为例,只关注“CommonModes”的部分:
void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) {
// 该Mode是CommonMode
if (modeName == kCFRunLoopCommonModes) {
// _commonModes存在则获取一份数据拷贝
CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
if (NULL == rl->_commonModeItems) {
// _commonModeItems不存在创建一个新的集合
rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
}
// 将source添加到_commonModeItems
CFSetAddValue(rl->_commonModeItems, rls);
if (NULL != set) {
CFTypeRef context[2] = {rl, rls};
// 调用__CFRunLoopAddItemToCommonModes函数向_commonModes中所有的Mode添加这个source
CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
CFRelease(set);
}
}
}
上面的source0/source1/observer/timer被统称为mode item
,一个item
可以被同时加入多个Mode。如果Mode里没有任何source0/source1/observer/timer,RunLoop便会立刻退出。
这也解决了一个问题--为什么列表滑动的时候,NSTimer不执行回调?该如何解决?
默认NSTimer是运行在RunLoop的kCFRunLoopDefaultMode
下,在列表滑动的时候,RunLoop会切换UITrackingRunLoopMode
,因为RunLoop只能运行在一种模式下,所以NSTimer不会执行回调。
使用现成的API将NSTimer就有添加到CommonModes就可以,kCFRunLoopDefaultMode
和 UITrackingRunLoopMode
都已经被标为”Common”属性的。这样Timer就同时加入了这两个Mode中。
CFRunLoopSourceRef
CFRunLoopSourceRef
对应着__CFRunLoopSource
结构体,其定义如下:
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
。
Source0
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);
// 当source被添加到RunLoop中后,会调用这个指针
void (*schedule)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
// 当调CFRunLoopSourceInvalidate函数移除该source的时候,会调用这个指针
void (*cancel)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
// RunLoop处理Source0的时候,会调用这个指针
void (*perform)(void *info);
} CFRunLoopSourceContext;
大神的博客中提到:Source0
并不能主动触发事件。使用时,你需要先调用CFRunLoopSourceSignal
,将这个Source标记为待处理,然后手动调用CFRunLoopWakeUp
来唤醒RunLoop,让其处理这个事件。
优秀的博客总是会被很多人阅读和模仿,这是可以理解的。但是确实没看到有人对这几句结论进行验证一下,当然我一开始也是看过记住,但是并没有做进一步的理解。
下面给出我自己的推导过程:
RunLoop通过__CFRunLoopDoSources0
函数处理Source0
。在它的实现有一段很关键的代码:
if (__CFRunLoopSourceIsSignaled(rls)) {
__CFRunLoopSourceUnsetSignaled(rls);
if (__CFIsValid(rls)) {
__CFRunLoopSourceUnlock(rls);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(rls->_context.version0.perform, rls->_context.version0.info);
CHECK_FOR_FORK();
sourceHandled = true;
} else {
__CFRunLoopSourceUnlock(rls);
}
}
将其简化一下:
if (__CFRunLoopSourceIsSignaled(rls)) {
__CFRunLoopSourceUnsetSignaled(rls);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(rls->_context.version0.perform, rls->_context.version0.info);
}
// __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__便是处理Source0的函数
// perform指针也是Source0中定义的,
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(void (*perform)(void *), void *info) {
if (perform) {
perform(info);
}
asm __volatile__(""); // thwart tail-call optimization
}
先判断该Source0
是否被标记,如果是,取消该Source0
的标记,并处理。既然这样肯定存在对应的一个标记函数__CFRunLoopSourceSetSignaled
:
// CFRunLoopSourceSignal函数是对外公开的。
void CFRunLoopSourceSignal(CFRunLoopSourceRef rls) {
CHECK_FOR_FORK();
__CFRunLoopSourceLock(rls);
if (__CFIsValid(rls)) {
__CFRunLoopSourceSetSignaled(rls);
}
__CFRunLoopSourceUnlock(rls);
}
关于CFRunLoopSourceSignal
函数的使用,CFRunLoop.c
并没有相关使用代码。但是在CFSocket.c
文件中能找到些许痕迹。
相关代码如下:
//
if (shared->_source) {
CFRunLoopSourceSignal(shared->_source);
_CFRunLoopSourceWakeUpRunLoops(shared->_source);
}
// CFRunLoopSourceContext代表Source0
sock->_shared->_source =
CFRunLoopSourceCreate(allocator, order, (CFRunLoopSourceContext *)&context);
if (sock->_shared->_source) {
CFRunLoopSourceSignal(sock->_shared->_source);
_CFRunLoopSourceWakeUpRunLoops(sock->_shared->_source);
}
// _CFRunLoopSourceWakeUpRunLoops是CFRunLoop.c中的内部方法
// 其核心就是调用CFRunLoopWakeUp函数
CF_PRIVATE void _CFRunLoopSourceWakeUpRunLoops(CFRunLoopSourceRef rls) {
CFBagRef loops = NULL;
__CFRunLoopSourceLock(rls);
if (__CFIsValid(rls) && NULL != rls->_runLoops) {
loops = CFBagCreateCopy(kCFAllocatorSystemDefault, rls->_runLoops);
}
__CFRunLoopSourceUnlock(rls);
if (loops) {
CFBagApplyFunction(loops, __CFRunLoopSourceWakeUpLoop, NULL);
CFRelease(loops);
}
}
static void __CFRunLoopSourceWakeUpLoop(const void *value, void *context) {
// 主动唤醒RunLoop
CFRunLoopWakeUp((CFRunLoopRef)value);
}
通过上面给出的相关代码,我想可以解释Source0
是如何被触发的了。
使用Source0
的情况:
-
触摸事件处理;
调用
performSelector:onThread:withObject:waitUntilDone:
方法;
Source1
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_t
,mach_port是用于内核向线程发送消息的。 注意:Source1在处理的时候会分发一些操作给Source0去处理。
使用Source1
的情况:
- 基于端口的线程间通信(A线程通过端口发送消息到B线程,这个消息是
Source1
的; - 系统事件的捕捉,以点击屏幕触发事件为例,我们点击屏幕到系统捕捉到这个点击事件是
Source1
,接着分发到Source0
去处理这个点击事件。
CFRunLoopObserverRef
CFRunLoopObserverRef
对应着__CFRunLoopObserver
结构体,实现如下:
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
都包含了一个回调(函数指针CFRunLoopObserverCallBack _callout
),当RunLoop的状态发生变化时,观察者就能通过回调接受到这个变化。
RunLoop有以下几种状态:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 结束休眠或被唤醒
kCFRunLoopExit = (1UL << 7), // 退出loop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
使用Observer
的情况:
- 用于监听RunLoop的状态;
- UI刷新(Before Waiting);
- AutoreleasePool释放;
CFRunLoopTimerRef
CFRunLoopTimerRef
对应着__CFRunLoopTimer
结构体,实现如下:
struct __CFRunLoopTimer {
CFRuntimeBase _base;
uint16_t _bits;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop;
CFMutableSetRef _rlModes;
CFAbsoluteTime _nextFireDate;
CFTimeInterval _interval; /* immutable */
CFTimeInterval _tolerance; /* mutable */
uint64_t _fireTSR; /* TSR units */
CFIndex _order; /* immutable */
CFRunLoopTimerCallBack _callout; /* immutable */
CFRunLoopTimerContext _context; /* immutable, except invalidation */
};
使用Timer
的情况:
- NSTimer,NSTimer基于RunLoop,其内部使用的就是
CFRunLoopTimerRef
; -
performSelector:withObject:afterDelay:
或类似带有afterDelay
的方法。
RunLoop运行
RunLoop通过CFRunLoopRun
和CFRunLoopRunInMode
这两个函数运行。
CFRunLoopRun
void CFRunLoopRun(void) {
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
函数默认在kCFRunLoopDefaultMode
下运行RunLoop,并且一直运行在一个do-while的循环里。
另外函数不会主动调用CFRunLoopStop
函数(kCFRunLoopRunStopped
)或者将所有事件源移除(kCFRunLoopRunFinished
)。
CFRunLoopRunInMode
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
无论是CFRunLoopRun
还是CFRunLoopRunInMode
都是调用了CFRunLoopRunSpecific
。
CFRunLoopRunSpecific
这里对CFRunLoopRunSpecific
函数的实现做了精简处理:
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {
// 第1步:通知Observers,进入loop
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
// 具体要做的事情
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
// 第10步:通知Observers,退出loop
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
return result;
}
__CFRunLoopRun
__CFRunLoopRun
可以说是RunLoop运行的核心方法。由于代码过长,这里对代码进行了精简:
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
int32_t retVal = 0;
do {
// 第2步:通知Observers,即将处理Timers
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
// 第3步:通知Observers,即将处理Source
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
// 处理Blocks
__CFRunLoopDoBlocks(rl, rlm);
// 第4步:处理Source0
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
if (sourceHandledThisLoop) {
// 处理Blocks
__CFRunLoopDoBlocks(rl, rlm);
}
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
// 第5步:判断有无Source1,有Source1,跳转到handle_msg
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
//
goto handle_msg;
}
didDispatchPortLastTime = false;
// 第6步:通知Observers,即将进入休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
// RunLoop休眠
__CFRunLoopSetSleeping(rl);
CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent();
// 第7步:等待别的消息来唤醒,如果没有被唤醒那就不会执行下面的代码
// 这些消息可能是:
// 一个基于port的Source的事件。
// 一个Timer到时间了
// RunLoop自身的超时时间到了
// 被其他什么调用者手动唤醒
__CFRunLoopServiceMachPort(waitSet,
&msg,
sizeof(msg_buffer),
&livePort, poll ? 0 : TIMEOUT_INFINITY,
&voucherState,
&voucherCopy);
rl->_sleepTime += (poll ? 0.0 : (CFAbsoluteTimeGetCurrent() - sleepStart));
// 取消RunLoop的休眠
__CFRunLoopUnsetSleeping(rl);
// 第8步:通知Observers,结束休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
// 判断RunLoop被唤醒的方式,并处理对应的事件
handle_msg:;
// 判断RunLoop被唤醒的方式
// MACH_PORT_NULL == livePort和livePort == rl->_wakeUpPort两种情况什么都不做,省略
// 被timer唤醒
else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
// 处理timer
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
}
// 被GCD唤醒
else if (livePort == dispatchPort) {
// 处理GCD
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}
// 被Source1唤醒
else {
// 处理Source1
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
}
// 处理Blocks
__CFRunLoopDoBlocks(rl, rlm);
// 第9步:决定RunLoop的返回值
if (sourceHandledThisLoop && stopAfterHandle) {
// 处理完事件就返回
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
// 超时
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
// RunLoop终止
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
// mode终止
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
retVal = kCFRunLoopRunFinished;
}
} while (0 == retVal);
return retVal;
}
RunLoop运行流程图:
上述过程中有两个地方要注意:
1.RunLoop处理GCD事件
在大多数情况,RunLoop和GCD各自有这自己的执行流程,不会出现依赖,但是有一种情况比较特殊。先看以下代码:
dispatch_async(dispatch_get_global_queue(0, 0), ^{
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"1111111");
});
});
打印函数调用栈:
使用GCD异步操作的时候,我们在一个子线程处理完一些事情后,要返回主线程处理事情的时候,这时候需要依赖于RunLoop。内部会调用__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
函数。
2.RunLoop休眠
当RunLoop一旦休眠意味着CPU不会分配任何资源,那线程也就没有事情干了,也进入休眠。RunLoop休眠内部是调用了mach_msg()
函数。操作系统中有内核层面的API和应用层面的API。内核层面的API是不会轻易暴露出来,mach_msg()
可以理解为是应用层面的API,告诉内核休眠该线程休眠。一旦接受到系统事件,也会转化成内核API,告诉内核需要唤醒该线程,那么又可以执行应用层API了。所以RunLoop的休眠可以看成是用户状态到内核状态的切换,而唤醒RunLoop就是内核状态到用户状态的切换。
总结
由于总结的东西相对来说比较多,会以面试题的形式单独写一篇RunLoop面试题分析来总结。
参考
《程序员的自我修养》
CoreFoundation源代码
深入理解RunLoop
苹果文档--RunLoop