一. RunLoop相关
什么是Runloop?
顾名思义,Runloop就是运行循环,就是在程序运行过程中循环做一些事情。
RunLoop的基本作用:
保持程序的持续运行
处理App中的各种事件(比如触摸事件、定时器事件等)
节省CPU资源,提高程序性能:该做事时做事,该休息时休息
......
1. UIApplicationMain里面的RunLoop
如果没有RunLoop
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"Hello, World!");
}
return 0;
}
执行完打印,就会退出程序。
如果有了RunLoop
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
UIApplicationMain函数里面就是创建一个RunLoop对象,然后RunLoop对象一直循环等待和处理消息,伪代码如下:
int main(int argc, char * argv[]) {
@autoreleasepool {
int retVal = 0;
do {
//睡眠中等待消息
int message = sleep_and_wait();
//处理消息
retVal = process_message(message);
} while (0 == retVal);
return 0;
}
}
这样就保证了程序并不会马上退出,一直保持运行状态。
2. RunLoop对象
iOS中有2套API来访问和使用RunLoop
Foundation:NSRunLoop
Core Foundation:CFRunLoopRef
获取RunLoop对象:
Foundation
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
Core Foundation
CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象
- NSRunLoop和CFRunLoopRef都代表着RunLoop对象
- NSRunLoop是基于CFRunLoopRef的一层OC包装
- CFRunLoopRef是开源的:Core Foundation源码
他们两者之间的关系如下图:
NSRunLoop就是对CFRunLoopRef的一层包装,就好像NSArray是对CFArrayRef的封装,NSString是对CFStringRef的封装一样。
获取当前线程、主线程RunLoop方法如下:
NSLog(@"%p %p", [NSRunLoop currentRunLoop], [NSRunLoop mainRunLoop]);
NSLog(@"%p %p", CFRunLoopGetCurrent(), CFRunLoopGetMain());
NSLog(@"%@", [NSRunLoop mainRunLoop]);
打印:
0x600001141260 0x600001141260
0x600000940a00 0x600000940a00
<CFRunLoop 0x600000940a00 [0x1089e6ae8]>{wakeup port = 0x1d07, stopped = false, ignoreWakeUps = false,
current mode = kCFRunLoopDefaultMode,
......
打印的地址不一样,可以发现使用%@打印mainRunLoop,里面装的的确是CFRunLoop,通过CFRunLoop的地址也可以验证:NSRunLoop的确是堆CFRunLoopRef的一层封装,所以地址不一样。
3. RunLoop与线程
- 每条线程都有唯一的一个与之对应的RunLoop对象,主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop,RunLoop会在线程结束时销毁。
- RunLoop是懒加载的,线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建。
- 主线程几乎所有的事情都是交给了runloop去做,比如UI界面的刷新、点击时间的处理、performSelector等等。
- RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value。
下载Core Foundation源码,拖到新创建的命令行项目中,查看CFRunLoopGetCurrent源码实现:
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
进入_CFRunLoopGet0:
static CFMutableDictionaryRef __CFRunLoops = NULL; //是个字典,线程作为key,取出对应的RunLoop
static CFLock_t loopsLock = CFLockInit;
// 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();
}
__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);
}
//将字典和key(线程)传入CFDictionaryGetValue函数,获取RunLoop对象
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
if (!loop) { //如果RunLoop不存在
CFRunLoopRef newLoop = __CFRunLoopCreate(t); //就创建RunLoop
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);//并且将RunLoop保存到字典里面
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;
}
其中__CFRunLoops是个字典,线程作为key,取出对应的RunLoop,如果没有,再创建,然后保存到字典中,验证了RunLoop对象的确是懒加载的。
4. RunLoop相关的类
Core Foundation中关于RunLoop的5个类
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
在源码中查看,CFRunLoopRef:
typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef;
可以发现CFRunLoopRef是指向__CFRunLoop结构体的指针,找到__CFRunLoop结构体源码:
struct __CFRunLoop {
......
pthread_t _pthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;//当前模式
CFMutableSetRef _modes; //是个集合,无序的,里面装的是CFRunLoopModeRef类型的对象
......
};
__CFRunLoop里面有个_modes,它是个集合,里面装的是一堆CFRunLoopModeRef类型的对象,当前的模式是_currentMode。
集合是无序的,数组是有序的,集合只能通过anyObject取值,数组可以通过索引取值,如下:
// 有序的
NSMutableArray *array;
[array addObject:@"123"];
array[0];
// 无序的
NSMutableSet *set;
[set addObject:@"123"];
[set anyObject];
进入CFRunLoopModeRef:
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
......
CFStringRef _name;//名称
CFMutableSetRef _sources0;//里面装的是CFRunLoopSourceRef类型的对象
CFMutableSetRef _sources1;//里面装的是CFRunLoopSourceRef类型的对象
CFMutableArrayRef _observers;//里面装的是CFRunLoopObserverRef类型的对象
CFMutableArrayRef _timers;//里面装的是CFRunLoopTimerRef类型的对象
......
};
① 5个类之间的关系:
CFRunLoopRef里面有个_modes集合,里面装好多CFRunLoopModeRef类型的模式,_currentMode是当前模式。
模式里面有name,_sources0、_sources1集合(里面装的是CFRunLoopSourceRef类型的东西),_observers数组(里面装的是CFRunLoopObserverRef类型的东西),_timers数组(里面装的是CFRunLoopTimerRef类型的东西)。
如下图所示:
- CFRunLoopModeRef代表RunLoop的运行模式
- 一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer
- RunLoop启动时只能选择其中一个Mode,作为currentMode,如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入
- 不同组的Source0/Source1/Timer/Observer能分隔开来,互不影响
- 如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出
- 其中Timer是定时器,平时创建的一些定时器都放在这里,Observer是监听器,source0/Source1是事件,比如点击事件、performSelector等等。
② 为什么多种模式要分开呢?
比如scrollView滚动的时候让它切换到滚动模式,那么在滚动模式下,scrollView就专心处理滚动相关的就可以了,以前模式下的事情就不处理了。如果不滚动,在正常模式下,就专心处理正常模式下的事情就好了,这样可以做到流畅不卡顿。
5. 常见的两种Mode
kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行。
UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
常用的就是默认模式和界面跟踪模式,其他模式基本用不到,都懒得说了。
6. Source0、Source1、Timers、Observers
RunLoop切换到某个模式就开始处理那个模式的Source0、Source1、Timers、Observers,那么这些东西分别代表什么呢?
Source0:触摸事件处理、performSelector:onThread:。
Source1:基于Port(端口)的线程间通信、系统事件捕捉 (比如点击事件,通过Source1捕捉,然后包装成Source0进行处理)。
Timers:NSTimer、performSelector:withObject:afterDelay:(底层就是NSTimer)。
Observers:用于监听RunLoop的状态、UI刷新(BeforeWaiting)、Autorelease pool(BeforeWaiting)。
比如,点击界面空白就是Source0事件,验证如下:
在touchesBegan:withEvent:方法打断点
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//打断点
}
查看函数调用栈:
可以发现的确是通过source0处理事件,最后才调用到touchesBegan:withEvent:方法的。
关于Observers监听UI刷新(BeforeWaiting):
比如以下代码就属于UI刷新:
self.view.backgroundColor = [UIColor redColor];
这句代码并不是立马执行,Observers会先记下来,当Observers监听到RunLoop将要睡觉啦,就在RunLoop将要睡觉之前执行(刷新UI)。
同理Autorelease pool也是一样,当Observers监听到RunLoop将要睡觉啦,就在RunLoop睡觉之前释放对象。
7. RunLoop状态
上面说了,Observers可以监听RunLoop的状态,那么RunLoop有几种状态呢?
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), //即将进入RunLoop
kCFRunLoopBeforeTimers = (1UL << 1), //即将处理Timer
kCFRunLoopBeforeSources = (1UL << 2), //即将处理Source
kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), //即将从休眠中唤醒
kCFRunLoopExit = (1UL << 7), //即将退出RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU //所有状态
};
8. 如何监听RunLoop的状态?
Observers数组里面有系统创建的一些Observer,用于监听RunLoop状态进行UI刷新、Autorelease pool等,如果我们自己想监听RunLoop状态肯定要自己创建Observer。
// 创建Observer
/*
参数一:一般传默认的:kCFAllocatorDefault
参数二:传入你想监听什么状态
参数三:是否重复监听
参数四:顺序0,不需要考虑顺序
参数五:监听函数名
参数六:context,会传入监听函数的info里面,一般传NULL
*/
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, observeRunLoopActicities, NULL);
// 添加Observer到RunLoop中
//kCFRunLoopCommonModes通用模式,默认包括kCFRunLoopDefaultMode和UITrackingRunLoopMode
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 释放
CFRelease(observer);
//监听RunLoop的函数,这个函数要求传入三个参数
void observeRunLoopActicities(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"kCFRunLoopEntry");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"kCFRunLoopBeforeTimers");
break;
case kCFRunLoopBeforeSources:
NSLog(@"kCFRunLoopBeforeSources");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"kCFRunLoopBeforeWaiting");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"kCFRunLoopAfterWaiting");
break;
case kCFRunLoopExit:
NSLog(@"kCFRunLoopExit");
break;
default:
break;
}
}
当然也可以通过block监听,和上面一样的:
// 创建Observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
......
}
});
// 添加Observer到RunLoop中
//kCFRunLoopCommonModes通用模式,默认包括kCFRunLoopDefaultMode和UITrackingRunLoopMode
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 释放
CFRelease(observer);
① 监听点击空白事件
点击空白,打印如下:
kCFRunLoopAfterWaiting
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources 即将处理source事件
-[ViewController touchesBegan:withEvent:]
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeWaiting
kCFRunLoopAfterWaiting
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
......
可以看出先是kCFRunLoopBeforeSources状态,之后再执行点击事件方法,这也验证了点击事件是source0事件。
② 监听定时器事件
添加定时器:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[NSTimer scheduledTimerWithTimeInterval:3.0 repeats:NO block:^(NSTimer * _Nonnull timer) {
NSLog(@"定时器-----------");
}];
// NSLog(@"%s",__func__);
}
点击空白,打印:
kCFRunLoopAfterWaiting
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeWaiting 先睡觉
kCFRunLoopAfterWaiting 3秒后唤醒
定时器-----------
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeWaiting
kCFRunLoopAfterWaiting
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
可以发现,先睡觉,睡了3秒后再执行定时器事件。
③ 监听模式切换
主view里面添加一个textView,写如下代码监听RunLoop模式改变:
// 创建Observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry: {
CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
NSLog(@"kCFRunLoopEntry - %@", mode);
CFRelease(mode);
break;
}
case kCFRunLoopExit: {
CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
NSLog(@"kCFRunLoopExit - %@", mode);
CFRelease(mode);
break;
}
default:
break;
}
});
// 添加Observer到RunLoop中
//kCFRunLoopCommonModes通用模式,默认包括kCFRunLoopDefaultMode和UITrackingRunLoopMode
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 释放
CFRelease(observer);
滑动textView,打印:
kCFRunLoopExit - kCFRunLoopDefaultMode
kCFRunLoopEntry - UITrackingRunLoopMode
kCFRunLoopExit - UITrackingRunLoopMode
kCFRunLoopEntry - kCFRunLoopDefaultMode
可以发现,先退出默认模式进入滑动模式,再退出滑动模式进入默认模式。
二. RunLoop的运行流程
1. 流程图
苹果官方的流程图:
官方的图有点抽象,看MJ老师的图:
对于第4步的block就是下面的block
CFRunLoopPerformBlock(<#CFRunLoopRef rl#>, <#CFTypeRef mode#>, ^{
//传入RunLoop和模式,RunLoop就会执行这个block里面的代码
})
2. 源码验证
仔细分析上图流程之后,下面我们查看源码,看看到底是不是这样的,但是RunLoop的入口是哪个呢?打断点,查看函数调用栈,如下:
可以发现第一个关于RunLoop的函数是CFRunLoopRunSpecific,是Core Foundation框架下的函数。在Core Foundation源码中搜索“ CFRunLoopRunSpecific”,由于原函数比较复杂,精简后如下:
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
//通知Observers进入Loop
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
//具体要做的事情
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
//通知Observers退出Loop
__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);
// 处理Source0
if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) {
// 如果返回YES,处理Blocks
__CFRunLoopDoBlocks(rl, rlm);
}
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
// 判断有无Source1
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
// 如果有Source1就跳转handle_msg
goto handle_msg;
}
// 通知Observers,即将休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
// 开始休眠
__CFRunLoopSetSleeping(rl);
CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent();
// 等待别的消息来唤醒当前线程,往下走代表有人唤醒它了
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
// 结束休眠
__CFRunLoopUnsetSleeping(rl);
// 通知Observers,结束休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
handle_msg:
if (被Timer唤醒) {
// 处理Timers
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
} else if (被GCD唤醒) {
// 处理GCD相关
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else { // 被Source1唤醒
// 处理Source1
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
}
// 再处理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;
}
voucher_mach_msg_revert(voucherState);
os_release(voucherCopy);
// 如果retVal等于0,继续执行循环
} while (0 == retVal);
return retVal;
}
上面源代码对比流程图可知,流程的确如图所示。
3. 小细节
①
__CFRunLoopDoObservers函数中真正做通知Observers相关的是CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION函数
__CFRunLoopDoBlocks函数中真正做Block相关的是CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK函数
__CFRunLoopDoSources0函数中真正做Source0相关的是CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION函数
__CFRunLoopDoTimers函数中真正做定时器相关的是CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION函数
__CFRunLoopDoSource函数中真正做Source1相关的是CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION函数
我们也可以通过函数调用栈来验证:
- (void)viewDidLoad {
[super viewDidLoad];
[NSTimer scheduledTimerWithTimeInterval:1.0 repeats:NO block:^(NSTimer * _Nonnull timer) {
NSLog(@"123"); // 断点
}];
}
在断点处查看函数调用栈:
可以发现的确调用了CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION函数,之后Core Foundation框架的函数就调用完了。
②
一般情况下GCD的东西是GCD来处理的,不会交给RunLoop。GCD是GCD,RunLoop是RunLoop,他们互不干扰,但是有一种情况下GCD是交给RunLoop处理的,如下:
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 子线程处理一些逻辑
// 回到主线程去刷新UI界面
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"11111111111"); // 断点
});
});
}
函数调用栈:
当我们在子线程处理一些逻辑然后回到主线程去刷新UI界面,这种情况就会交给RunLoop去处理GCD相关的东西,然后再回到GCD。
③ 线程休眠
当RunLoop事情处理完发现没有事情干,就会进入休眠,这时候你可以认为是“线程阻塞”,这时候代码就不会往下走了,直到被唤醒,才会继续往下走。
这种休眠和while(1){};不一样,这种休眠是当前线程休息了,CPU不再给当前线程分配资源,但是while(1){};这种阻塞是一个死循环,这时候线程没有休息,还在一直执行while(1){}。
那么线程休眠是怎么做到的呢?
同样查看源码,进入__CFRunLoopServiceMachPort函数:
......
ret = mach_msg(msg, MACH_RCV_MSG|(voucherState ? MACH_RCV_VOUCHER : 0)|MACH_RCV_LARGE|((TIMEOUT_INFINITY != timeout) ? MACH_RCV_TIMEOUT : 0)|MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0)|MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AV), 0, msg->msgh_size, port, timeout, MACH_PORT_NULL);
......
上面的mach_msg是内核层面的API,内核层面的API是不给我们程序员使用的,因为比较危险,给我们使用的都是应用层面的API。
RunLoop休眠的实现原理,如下图:
当用户态调用mach_msg()函数会自动切换到内核态,会真正调用内核态的mach_msg()函数,内核态mach_msg做的事就是:没有消息就让线程休眠,有消息就唤醒线程,唤醒线程后就又回到用户态。所以线程休眠就是因为用户态和内核态的切换。
三. RunLoop在实际开发中的应用
- 解决NSTimer在滑动时停止工作的问题
- 控制线程生命周期(线程保活)
- 监控应用卡顿
- 性能优化
先讲前两种,后面两种讲到性能优化再说。
1. 解决NSTimer在滑动时停止工作的问题
定时器失效问题:
在view上添加一个scrollView,写如下代码:
- (void)viewDidLoad {
[super viewDidLoad];
static int count = 0;
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"%d", ++count);
}];
}
不拖动scrollView,定时器一直在打印,拖动scrollView发现定时器停止工作了。
这是因为RunLoop只会在一种模式下工作,默认情况下定时器被添加到NSDefaultRunLoopMode模式下,但是拖动的时候RunLoop切换成UITrackingRunLoopMode模式,但是定时器没有添加到这个模式下,所以定时器不会工作。
解决办法也很简单,就是在两种模式下都添加定时器,代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
static int count = 0;
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"%d", ++count);
}];
// NSDefaultRunLoopMode、UITrackingRunLoopMode才是真正存在的模式
// NSRunLoopCommonModes并不是一个真的模式,它只是一个标记
// timer能在_commonModes数组中存放的模式下工作
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
{
//scheduledTimerWithTimeInterval默认会把定时器添加到默认模式下,所以用timerWithTimeInterval
[NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"%d", ++count);
}];
}
注意,NSRunLoopCommonModes并不是一个真的模式,它只是一个标记,定时器能在_commonModes数组中存放的模式下工作。
如果timer被标记为commonModes,那么timer就能在_commonModes数组中存放的模式下工作,而_commonModes数组中存放的恰好是NSDefaultRunLoopMode和UITrackingRunLoopMode模式,当然这两种模式也在_modes数组里面。
能在commonModes“模式”下工作的东西都会被添加到_commonModeItems数组中,比如上面的timer。
再看一下__CFRunLoop结构体源码:
struct __CFRunLoop {
......
pthread_t _pthread;
CFMutableSetRef _commonModes; //就是这个_commonModes数组
CFMutableSetRef _commonModeItems; //能在commonModes“模式”下工作的东西都会被添加到这个数组中
CFRunLoopModeRef _currentMode;//当前模式
CFMutableSetRef _modes; //是个集合,无序的,里面装的是CFRunLoopModeRef类型的对象
......
};
如果timer是NSDefaultRunLoopMode或者UITrackingRunLoopMode模式,那么它就会被添加到自己模式下的_timers数组中。
进入CFRunLoopModeRef:
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
......
CFStringRef _name;//名称
CFMutableSetRef _sources0;//里面装的是CFRunLoopSourceRef类型的对象
CFMutableSetRef _sources1;//里面装的是CFRunLoopSourceRef类型的对象
CFMutableArrayRef _observers;//里面装的是CFRunLoopObserverRef类型的对象
CFMutableArrayRef _timers;//里面装的是CFRunLoopTimerRef类型的对象
......
};
关于线程保活请看下篇文章。
四. 面试题
讲讲 RunLoop,项目中有用到吗?
runloop内部实现逻辑?
看流程图runloop和线程的关系?
一对一的关系timer 与 runloop 的关系?
① RunLoop对象里面有个_modes数组,里面放一堆模式,模式里面会放timer,如果timer被标记为commonModes,那么timer就能在_commonModes数组中存放的模式下工作,能在commonModes“模式”下工作的东西都会被添加到_commonModeItems数组里中。
② 如果线程休眠了,timer也可以唤醒休眠的RunLoop。程序中添加每3秒响应一次的NSTimer,当拖动tableview时timer可能无法响应要怎么解决?
将timer被标记为commonModesrunloop 是怎么响应用户操作的,具体流程是什么样的?
当用户有个点击事件,这个系统事件会先被Source1捕捉,Source1捕捉之后会包装成事件队列(EventQuene),再放到Source0里面进行处理,然后RunLoop循环再处理Source0里面的事件。说说runLoop的几种状态
runloop的mode作用是什么?
不同模式的Source0/Source1/Timer/Observer能分隔开来,互不影响
Demo地址:RunLoop1