iOS底层原理探索—RunLoop的本质
探索底层原理,积累从点滴做起。大家好,我是Mars。
往期回顾
iOS底层原理探索—OC对象的本质
iOS底层原理探索—class的本质
iOS底层原理探索—KVO的本质
iOS底层原理探索— KVC的本质
iOS底层原理探索— Category的本质(一)
iOS底层原理探索— Category的本质(二)
iOS底层原理探索— 关联对象的本质
iOS底层原理探索— block的本质(一)
iOS底层原理探索— block的本质(二)
iOS底层原理探索— Runtime之isa的本质
iOS底层原理探索— Runtime之class的本质
iOS底层原理探索— Runtime之消息机制
今天带领大家继续探索RunLoop
的本质
RunLoop
顾名思义,运行循环,在程序运行过程中循环做一些事情。如果没有RunLoop
,程序执行完毕就会立即退出,如果有RunLoop
程序并不会马上退出,而是保持运行状态,等待处理程序的各种事件。
RunLoop
可以保持程序的持续运行,在没有事件处理的时候使程序进入休眠模式,从而节省CPU
资源,提高程序性能。
我们可以看到,程序在打印完
Hello,World!
后输出一句Program ended with exit code: 0
,说明程序结束了,这是没有RunLoop
的情况。有RunLoop.png
有
RunLoop
,程序就不会马上退出,而是保持运行状态。苹果官方文档中用一张示意图为我们展示了
RunLoop
的运行逻辑:RunLoop运行逻辑.jpg
从图中可以看出,
RunLoop
在运行循环过程中,接收到Input sources
或者Timer sources
时会通过对应处理方式处理;没有事件消息传入的时候,就会使程序处于休眠状态。
我们进入上面有RunLoop
的代码中的UIApplicationMain
内部查看,发现返回值是int
类型,那么我们对上面的main
函数做一些改动:
通过运行程序发现只打印
开始了
,并不会打印结束了
。我们知道在程序运行时会开启一条主线程,通过测试说明在UIApplicationMain
函数中,开启了一个和主线程相关的RunLoop
,导致UIApplicationMain
不会返回,一直在运行中,也就保证了程序的持续运行。RunLoop
在底层中有部分是开源的,我们可以进入官方文档下载源码帮助我们分析。下面来看
RunLoop
的源码:RunLoop源码.png
我们发现
RunLoop
底层其实是一个do while
循环,通过判断result
的值来实现。因此,我们可以把RunLoop
看成一个死循环。如果没有RunLoop
,UIApplicationMain
函数执行完毕之后将直接返回,也就没有程序持续运行一说了。
RunLoop基本作用
1、保持程序持续运行:程序一启动就会开一个主线程,主线程一开起来就会跑一个主线程对应的RunLoop,RunLoop保证主线程不会被销毁,也就保证了程序的持续运行。
2、处理App中的各种事件:比如触摸事件、定时器事件、Selector事件等。
3、节省CPU资源,提高程序性能:程序运行起来时,当什么操作都没有做的时候,RunLoop
就告诉CUP
,现在没有事情做,我要去休息,这时CUP
就会将其资源释放出来去做其他的事情,当有事情做的时候RunLoop
就会立马起来去做事情。
RunLoop对象
在iOS
中提供了两套API
来访问和使用RunLoop
:Fundation
框架中的NSRunLoop
对象、CoreFoundation
中的CFRunLoopRef
对象。
我们知道,CoreFoundation
是一套C语言
的API
,而Fundation
框架则是基于CoreFoundation
用OC
语言封装的。CFRunLoopRef
的源码是开源的,我们可以从官方文档下载源码,接下来的分析也是基于CFRunLoopRef
对象进行分析。
获取RunLoop对象
//Foundation
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
//Core Foundation
CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象
RunLoop与线程的关系
上文通过CFRunLoopGetCurrent();
函数获得当前线程的RunLoop
对象,我们进入源码查看:
// 获得当前线程的RunLoop对象,内部调用_CFRunLoopGet0函数
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);
// 根据传入的主线程获取主线程对应的RunLoop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
// 保存主线程 将主线程-key和RunLoop-Value保存到字典中
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
// 从字典里面拿,将线程作为key从字典里获取一个loop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
// 如果loop为空,则创建一个新的loop,所以runloop会在第一次获取的时候创建
if (!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
// 创建好之后,以线程为key runloop为value,一对一存储在字典中,下次获取的时候,则直接返回字典内的runloop
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);
//线程结束是销毁loop
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;
}
通过源码分析可以看出,线程和RunLoop
之间是一一对应的,其关系是保存在一个Dictionary
字典里。所以我们创建子线程RunLoop
时,只需在子线程中获取当前线程的RunLoop
对象即可[NSRunLoop currentRunLoop];
。如果不获取,那子线程就不会创建与之相关联的RunLoop
,并且只能在一个线程的内部获取其RunLoop
。
当通过调用[NSRunLoop currentRunLoop];
方法获取RunLoop
时,会先看一下字典里有没有子线程对应的RunLoop
,如果有则直接返回RunLoop
,如果没有则会创建一个,并将与之对应的子线程存入字典中。当线程结束时,RunLoop
会被销毁。
分析至此我们可以得出总结:
1、每条线程都有唯一的一个与之对应的
RunLoop
对象
2、RunLoop
保存在一个全局的Dictionary
里,线程作为key
,RunLoop
作为value
3、主线程的RunLoop
已经自动创建好了,子线程的RunLoop
需要主动创建
4、RunLoop
在第一次获取时创建,在线程结束时销毁
RunLoop底层结构
在源码中找到__CFRunLoop
类型的结构体:
我们重点分析
CFRunLoopModeRef _currentMode;
和CFMutableSetRef _modes;
这两个成员变量:
CFMutableSetRef _modes;
是一个集合,里面包含一个或者多个model
;
CFRunLoopModeRef _currentMode;
是当前的model
,代表RunLoop的运行模式,指向__CFRunLoopMode
结构体的指针。
我们查看一下__CFRunLoopMode
结构体源码:
红色标注的代码,有两个集合
Set
,两个数组Array
,里面分别保存着Source0
、Source1
、Timers
、Observer
,它们分别代表什么呢?
Source0: 触摸事件处理、performSelector:onThread:
Source1: 基于Port的线程间通信、系统事件捕捉
Timers:NSTimer、performSelector:withObject:afterDelay:
Observers: 用于监听RunLoop的状态、UI刷新(BeforeWaiting)、Autorelease pool(BeforeWaiting)
通过上面分析我们知道,CFRunLoopModeRef
代表RunLoop
的运行模式,一个RunLoop
包含若干个Mode
——CFRunLoopMode
,每个Mode
又包含若干个Source0
、Source1
、Timer
、Observer
,而RunLoop
启动时只能选择其中一个Mode
作为currentMode
。
通过上面的分析,我们对RunLoop
内部结构有了大致的了解,接下来分析RunLoop
中的相关类都有哪些及作用。
RunLoop相关类及作用
Core Foundation
中提供了关于RunLoop
的5个类:
1、
CFRunLoopRef
:获得当前RunLoop
和主RunLoop
2、
CFRunLoopModeRef
:运行模式3、
CFRunLoopSourceRef
:事件源,输入源4、
CFRunLoopTimerRef
:定时器时间5、
CFRunLoopObserverRef
:观察者
1. CFRunLoopModeRef
CFRunLoopModeRef
代表RunLoop
的运行模式。
一个RunLoop
包含若干个Mode
,每个Mode
又包含若干个Source0
、Source1
、Timer
、Observer
。
每次
RunLoop
启动时,只能指定其中一个Mode
,这个Mode
被称作 CurrentMode
。如果需要切换Mode
,只能退出RunLoop
,再重新指定一个Mode
进入,这样做主要是为了分隔开不同组的Source0
、Source1
、Timer
、Observer
,让其互不影响。如果Mode
里没有任何Source0
、Source1
、Timer
、Observer
,RunLoop
会立马退出
注意:一种Mode中可以有多个Source0
、Source1
、Timer
、Observer
。但是必须至少有一个Source
或者Timer
,因为如果Mode
为空,RunLoop
运行到空模式不会进行空转,就会立刻退出。
系统默认注册的5个Mode:
RunLoop
有五种运行模式:
kCFRunLoopDefaultMode
:App
的默认Mode
,通常主线程是在这个Mode
下运行2.
UITrackingRunLoopMode
:界面跟踪Mode
,用于ScrollView
追踪触摸滑动,保证界面滑动时不受其他Mode
影响3.
UIInitializationRunLoopMode
: 在刚启动App
时第进入的第一个Mode
,启动完成后就不再使用,会切换到kCFRunLoopDefaultMode
4.
GSEventReceiveRunLoopMode
: 接受系统事件的内部Mode
,通常用不到5.
kCFRunLoopCommonModes
: 这是一个占位用的Mode
,作为标记kCFRunLoopDefaultMode
和UITrackingRunLoopMode
用,并不是一种真正的Mode
2、CFRunLoopSourceRef事件源(输入源)
Source
分为Source0
、Source1
两种
Source0
:非基于Port
的,用于用户主动触发的事件(点击button
或点击屏幕)
Source1
:基于Port
的,通过内核和其他线程相互发送消息(与内核相关)
3、CFRunLoopObserverRef
CFRunLoopObserverRef
是观察者,能够监听RunLoop
的状态改变,包括唤醒,休息,以及处理各种事件。
系统为我们提供了一下几种状态:
RunLoop的运行逻辑
在文章开头我们简要概括了一下RunLoop
的运行逻辑,下面通过源码分析来详细阐述一下(源码已经做过简化,只介绍主要流程):
// 公开的CFRunLoopRun方法,其内部会调用CFRunLoopRunSpecific
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
// 经过精简的 CFRunLoopRunSpecific 函数代码,其内部会调用__CFRunLoopRun函数
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
// 通知Observers : 进入Loop
// __CFRunLoopDoObservers内部会调用 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__函数
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
// 核心的Loop逻辑
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
// 通知Observers : 退出Loop
if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
return result;
}
// 精简后的 __CFRunLoopRun函数,保留了主要代码
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
int32_t retVal = 0;
do {
// 通知Observers:即将处理Timers
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
// 通知Observers:即将处理Sources
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
// 处理Blocks
__CFRunLoopDoBlocks(rl, rlm);
// 处理Sources0
if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) {
// 处理Blocks
__CFRunLoopDoBlocks(rl, rlm);
}
//判断有没有Sources1
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
// 如果有Sources1,就跳转到handle_msg标记处
goto handle_msg;
}
// 通知Observers:即将休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
// 进入休眠,等待其他消息唤醒
__CFRunLoopSetSleeping(rl);
__CFPortSetInsert(dispatchPort, waitSet);
do {
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
} while (1);
// 醒来
__CFPortSetRemove(dispatchPort, waitSet);
__CFRunLoopUnsetSleeping(rl);
// 通知Observers:已经唤醒
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
handle_msg: // 看看是谁唤醒了RunLoop,进行相应的处理
if (被Timer唤醒的) {
// 处理Timer
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
}
else if (被GCD唤醒的) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else { // 被Sources1唤醒的
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply);
}
// 执行Blocks
__CFRunLoopDoBlocks(rl, rlm);
// 根据之前的执行结果,来决定怎么做,为retVal赋相应的值
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运行逻辑源码梳理一下:
RunLoop运行逻辑.png
RunLoop退出
RunLoop
在一下三种情况下回退出:
1、主线程销毁,
RunLoop
退出;
2、Mode
中有一些Timer
、Source
、Observer
,这些保证Mode
不为空时,RunLoop
没有空转并且是在运行的。当Mode
中为空的时候,RunLoop
会立刻退出;
3、启动RunLoop
的时候可以设置什么时候停止;
手动设置RunLoop
的停止时间:
[NSRunLoop currentRunLoop]runUntilDate:<#(nonnull NSDate *)#>
[NSRunLoop currentRunLoop]runMode:<#(nonnull NSString *)#> beforeDate:<#(nonnull NSDate *)#>
对于RunLoop
的本质分析到此就结束了,下篇我们继续分析RunLoop
的应用。
更多技术知识请关注公众号
iOS进阶
iOS进阶.jpg