开胃面试题
1.讲讲 RunLoop,项目中有用到吗?
2.RunLoop内部实现逻辑?
2.Runloop和线程的关系?
3.timer 与 Runloop 的关系?
4.程序中添加每隔几秒就响应一次的NSTimer,当拖动tableview时,timer可能无法响应要怎么解决?
6.Runloop 是怎么响应用户操作的, 具体流程是什么样的?
7.说说RunLoop的几种状态?
8.Runloop的mode作用是什么?
看这篇文章之前可以先回答一下这几个面试题,然后带着问题耐心看完这篇文章,再来回答一下看看
一、什么是RunLoop?
顾名思义, RunLoop就是运行循环, 它在程序运行过程中交替循环做一些事情,如果没有RunLoop,程序执行完毕就会立即退出,如果有RunLoop,程序会一直运行,并且随时响应用户的操作。在没有用户操作的时候就睡觉,充分节省CPU资源,提高程序性能。
二、RunLoop有什么用?
1.保持程序持续运行,iOSApp一启动就会开一个主线程,主线程会开启RunLoop,保证主线程不会被销毁,也就保持了程序持续运行(命令行项目没有开启RunLoop,所以程序执行完就退出了)
2.处理App中各种事件,如触摸事件,定时器事件,Selector事件,网络请求, 线程间的通信,界面刷新,AutoreleasePool释放对象等。
3.节省CPU资源,提高程序性能,iOSApp启动后,当没有事情要做的时候,RunLoop就会睡觉,节省CPU资源。等到有事要做的时候,就会马上去做事。
如果没有RunLoop, 像下面这样, 这是一个macOS Command Line Tool程序, 打印完"Hello, World!, 程序就会退出, 表现在App上就是App一打开就闪退了.
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
}
return 0;
}
2019-06-21 20:00:06.877983+0800 NoRunLoop[44082:4739231] Hello, World!
Program ended with exit code: 0
我们可以通过一张图来看一下RunLoop内部运行逻辑
三、RunLoop对象
iOS中有2套API来访问和使用RunLoop
Core Foundation : CFRunLoopRef
它是开源的: https://opensource.apple.com/tarballs/CF/Foundation : NSRunLoop (基于CFRunLoopRef的OC封装)
NSRunLoop和CFRunLoopRef都代表着RunLoop对象
获取RunLoop对象
Foundation:
[NSRunLoop currentRunLoop]; //获取当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; //获取主线程的RunLoop对象
Core Foundation
CFRunLoopGetCurrent(); //获取当前线程的RunLoop对象
CFRunLoopGetMain(); //获取主线程的RunLoop对象
四、RunLoop在哪里开启
应用要保持运行状态,必须开启一个RunLoop,主线程一开起来,RunLoop就在程序的入口main函数中开启了。
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
RunLoop在UIApplicationMain函数内启动,进入UIApplicationMain
UIKIT_EXTERN int UIApplicationMain(int argc, char *argv[], NSString * __nullable principalClassName, NSString * __nullable delegateClassName);
可以看到,它返回的是一个int类型的数据,我们对它做一些修改看看
int main(int argc, char * argv[]) {
@autoreleasepool {
NSLog(@"开始");
int re = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
NSLog(@"结束");
return re;
}
}
执行程序,可以看到只会打印开始,不会打印结束,这说明在UIApplicationMain函数中,开启了RunLoop,这个RunLoop保活了主线程,也就保持了程序持续运行。
RunLoop中的源码
// 用DefaultMode启动
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
可以看到,RunLoop是使用do while判断result的值是否退出程序实现的。所以,可以将RunLoop看成一个死循环,如果没有这个死循环,UIApplicationMain函数执行完将直接返回,也就没有程序持续运行了。
五、RunLoop与线程的关系
- 每条线程都有唯一的一个与之对应的RunLoop对象
- RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
- 线程刚创建时并没有RunLoop对象, RunLoop会在第一次获取它时创建,RunLoop会在线程结束时销毁
- 主线程的RunLoop程序会自动获取(创建), 子线程默认没有开启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);
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;
}
可以看到
1. 线程和RunLoop是一一对应的,它们保存在一个字典里。我们创建子线程的RunLoop时,在子线程中获取当前线程的RunLoop对象即可。如果不获取,子线程是不会创建对应的RunLoop的,并且RunLoop只能在一个线程的内部获取。
2. 方法[NSRunLoop currentRunLoop]调用时,会先看一下字典里有没有子线程对应的RunLoop,如果有则直接返回,如果没有则会创建一个,并与子线程一起存入字典。当线程结束时,RunLoop也会被销毁。
六、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;
};
除一些记录属性外,主要来看一下一下两个成员变量
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
CFRunLoopModeRef其实是指向__CFRunLoopMode结构体的指针,__CFRunLoop结构体的源码如下
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 */
};
源码里面很多东西,我们主要查看以下成员变量
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
上面的代码中,CFRunLoopModeRef代表RunLoop的运行模式,一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0、Source1、Timer、Observer,而RunLoop启动时只能选择其中一个Mode作为currentMode。
名词解析
Source0:触摸事件,PerformSelectors
Source1:基于Port的线程间通信
Timer:定时器,NSTimer
Observer:监听器,用于监听RunLoop的状态
我们可以通过打断点,使用bt
打印堆栈信息稍微看一下
触摸事件
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"点击了屏幕");
}
可以看到,触摸事件是会触发Source0
performSelector
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self performSelectorOnMainThread:@selector(test) withObject:nil waitUntilDone:YES];
});
NSTimer
[NSTimer scheduledTimerWithTimeInterval:3.0 repeats:NO block:^(NSTimer * _Nonnull timer) {
NSLog(@"NSTimer ---- timer调用了");
}];
七、RunLoop相关的类和作用
Core Foundation中关于RunLoop的5个类
-
CFRunLoopRef
- 获得当前RunLoop的主线程RunLoop -
CFRunLoopModeRef
- RunLoop运行模式,只能选择一种,在不同模式中做不同的操作 -
CFRunLoopSourceRef
- 事件源,输入源 -
CFRunLoopTimerRef
- 定时器事件 -
CFRunLoopObserverRef
- 观察者
我们可以在开源的CFRunLoopRef看到它们之间的关系,源代码里面不止这些, 为了方便这里只拿了部分比较有用的.
我们可以看到CFRunLoopRef里面有CFRunLoopModeRef
// CFRunLoopRef里面有CFRunLoopModeRef
typedef struct __CFRunLoop * CFRunLoopRef;
struct __CFRunLoop {
pthread_t _pthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
};
CFRunLoopModeRef里有CFRunLoopSourceRef,CFRunLoopTimerRef、CFRunLoopObserverRef
// CFRunLoopModeRef里有CFRunLoopSourceRef、CFRunLoopTimerRef、CFRunLoopObserverRef
typedef struct __CFRunLoopMode * CFRunLoopModeRef;
struct __CFRunLoopMode {
CFStringRef _name;
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
}
1.CFRunLoopModeRef
它代表RunLoop的运行模式
- 一个RunLoop包含若干个Mode, 每个Mode又包含若干个Souce0/Souce1/Timer/Observer
- RunLoop启动时只能选择其中一个Mode, 作为当前模式currentMode
- 如果需要切换Mode, 只能退出当前Loop, 再重新选择一个Mode进入, 这样不同组的Source0/Source1/Timer/Observer能分隔开来, 互不影响
- 如果Mode里没有任何Source0/Source1/Timer/Observer, RunLoop会立马退出
系统默认注册的5个Mode
RunLoop有如下5种运行模式,其中常见的有2种,分别是KCFRunLoopDefaultMode和UITrackingRunLoopMode
-
KCFRunLoopDefaultMode(NSDefaultRunLoopMode)
, 这是App的默认Mode, 通常主线程是在这个Mode下运行 -
UITrackingRunLoopMode
, 这是界面跟踪Mode, 用于ScrollView追踪触摸滑动, 保证界面滑动时不受其它Mode影响 -
UIInitializationRunLoopMode
,在刚启动App时进入的第一个Mode,启动完成后就不再使用,会切换到KCFRunLoopDefaultMode -
KCFRunLoopCommonModes
,这是一个占位用的Mode,作为标记KCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一种真正的Mode
Mode间的切换
我们平时在开发中一定遇到过,当我们使用NSTimer每一段时间执行一些事情时滑动UIScrollView,NSTimer就会暂停,当我们停止滑动后,NSTimer又会重新恢复,我们通过一段代码来看一下
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];
// 加入到RunLoop中才可以运行
// 1. 把定时器添加到RunLoop中,并且选择默认运行模式NSDefaultRunLoopMode = kCFRunLoopDefaultMode
// [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// 当textFiled滑动的时候,timer失效,停止滑动时,timer恢复
// 原因:当textFiled滑动的时候,RunLoop的Mode会自动切换成UITrackingRunLoopMode模式,因此timer失效,当停止滑动,RunLoop又会切换回NSDefaultRunLoopMode模式,因此timer又会重新启动了
// 2. 当我们将timer添加到UITrackingRunLoopMode模式中,此时只有我们在滑动textField时timer才会运行
// [[NSRunLoop mainRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
// 3. 那个如何让timer在两个模式下都可以运行呢?
// 3.1 在两个模式下都添加timer 是可以的,但是timer添加了两次,并不是同一个timer
// 3.2 使用站位的运行模式 NSRunLoopCommonModes标记,凡是被打上NSRunLoopCommonModes标记的都可以运行,下面两种模式被打上标签
//0 : <CFString 0x10b7fe210 [0x10a8c7a40]>{contents = "UITrackingRunLoopMode"}
//2 : <CFString 0x10a8e85e0 [0x10a8c7a40]>{contents = "kCFRunLoopDefaultMode"}
// 因此也就是说如果我们使用NSRunLoopCommonModes,timer可以在UITrackingRunLoopMode,kCFRunLoopDefaultMode两种模式下运行
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
NSLog(@"%@",[NSRunLoop mainRunLoop]);
}
-(void)show
{
NSLog(@"-------");
}
上述代码中,NSTimer在我们滑动ScrollView时,就不再管用了,调用的方法也就不再执行了。因为我们在主线程使用定时器,此时RunLoop的Mode为KCFRunLoopDefaultMode,即这个定时器属于KCFRunLoopDefaultMode。但是我们滑动ScrollView时,RunLoop的Mode会切换到UITrackingRunLoopMode,所以此时的定时器就失效了。当我们停止滑动时,RunLoop的Mode会切换回KCFRunLoopDefaultMode,所以NSTimer就又管用了。
同样道理的还有ImageView的显示,我们直接来看代码,不在赘述了
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"%s",__func__);
// performSelector默认是在default模式下运行,因此在滑动ScrollView时,图片不会加载
// [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"abc"] afterDelay:2.0 ];
// inModes: 传入Mode数组
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"abc"] afterDelay:2.0 inModes:@[NSDefaultRunLoopMode,UITrackingRunLoopMode]];
我们使用GCD也可以创建定时器,而且更为精确
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//创建队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
//1.创建一个GCD定时器
/*
第一个参数:表明创建的是一个定时器
第四个参数:队列
*/
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 需要对timer进行强引用,保证其不会被释放掉,才会按时调用block块
// 局部变量,让指针强引用
self.timer = timer;
//2.设置定时器的开始时间,间隔时间,精准度
/*
第1个参数:要给哪个定时器设置
第2个参数:开始时间
第3个参数:间隔时间
第4个参数:精准度 一般为0 在允许范围内增加误差可提高程序的性能
GCD的单位是纳秒 所以要*NSEC_PER_SEC
*/
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
//3.设置定时器要执行的事情
dispatch_source_set_event_handler(timer, ^{
NSLog(@"---%@--",[NSThread currentThread]);
});
// 启动
dispatch_resume(timer);
}
2.CFRunLoopSourceRef事件源(输入源)
Source分为两种
Source0
:非基于Port的,用于用户主动触发的事件(点击Button或点击屏幕等)
Source1
:基于Port的,通过内核和其他线程相互发送消息(与内核相关)
触摸事件和PerformSelector会触发Source0事件,我们在前面介绍过
3.CFRunLoopObserverRef
CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变。下面我们通过给RunLoop添加监听者,监听其运行状态
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//创建监听者
/*
第一个参数 CFAllocatorRef allocator:分配存储空间 CFAllocatorGetDefault()默认分配
第二个参数 CFOptionFlags activities:要监听的状态 kCFRunLoopAllActivities 监听所有状态
第三个参数 Boolean repeats:YES:持续监听 NO:不持续
第四个参数 CFIndex order:优先级,一般填0即可
第五个参数 :回调 两个参数observer:监听者 activity:监听的事件
*/
/*
所有事件
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
};
*/
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"RunLoop进入");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"RunLoop要处理Timers了");
break;
case kCFRunLoopBeforeSources:
NSLog(@"RunLoop要处理Sources了");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"RunLoop要休息了");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"RunLoop醒来了");
break;
case kCFRunLoopExit:
NSLog(@"RunLoop退出了");
break;
default:
break;
}
});
// 给RunLoop添加监听者
/*
第一个参数 CFRunLoopRef rl:要监听哪个RunLoop,这里监听的是主线程的RunLoop
第二个参数 CFRunLoopObserverRef observer 监听者
第三个参数 CFStringRef mode 要监听RunLoop在哪种运行模式下的状态
*/
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
/*
CF的内存管理(Core Foundation)
凡是带有Create、Copy、Retain等字眼的函数,创建出来的对象,都需要在最后做一次release
GCD本来在iOS6.0之前也是需要我们释放的,6.0之后GCD已经纳入到了ARC中,所以我们不需要管了
*/
CFRelease(observer);
}
输出
可以看到,Observer确实用来监听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,就跳转到handle_msg标记处
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
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重复做着这些事情,图中的序号就是RunLoop的执行步骤。
相关解释:
- Source0: 触摸事件处理, performSelector:onThread:
- Source1: 基于Port的线程间通信, 系统事件捕捉
- Timers: NSTimers, performSelector:withObject:afterDelay:
- Observers: 用于监听RunLoop的状态, UI刷新(BeforWaiting), Autorelease pool(BeforeWaiting)
九、RunLoop的退出
- 主线程销毁RunLoop退出。
- Mode中有一些Timer、Source、Observer,这些保证Mode不为空时RunLoop没有空转并且是在运行的,当Mode为空时,RunLoop会立即退出。
- 我们在启动RunLoop的时候可以设置什么时候停止。
十、RunLoop的应用
1、使用 RunLoop 控制线程生命周期(线程保活)
在我们的iOS程序中,开启一个子线程执行任务的时候,它执行完任务就会自行销毁,等到再要执行这样的任务的时候,又要重新开启子线程,如果经常需要用到子线程,老是这样开启和销毁子线程是比较耗费资源的,这个时候为了节约资源,我们可以使用RunLoop来保活子线程,提高性能,比如我们经常用到的网络请求框架AFNetworking就是通过RunLoop保活线程来提高性能的。
下面是一个点击屏幕就开启一个子线程进行打印的程序,为了监听线程是否挂了,我们创建一个继承自NSThread的类,里面只做一件事情,就是dealloc方法里面打印它是否挂了。接着,我们在点击事件中开启子线程打印。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
QLThread *thread = [[QLThread alloc] initWithTarget:self selector:@selector(printThread) object:nil];
[thread start];
}
- (void)printThread {
NSLog(@"%s thread:%@",__func__, [NSThread currentThread]);
}
2019-06-21 22:28:07.166746+0800 RunLoop[64726:7390313] -[ViewController printThread] thread:<QLThread: 0x6000008df100>{number = 3, name = (null)}
2019-06-21 22:28:07.167628+0800 RunLoop[64726:7390313] -[QLThread dealloc]
2019-06-21 22:28:10.077604+0800 RunLoop[64726:7390336] -[ViewController printThread] thread:<QLThread: 0x6000008d10c0>{number = 4, name = (null)}
2019-06-21 22:28:10.078640+0800 RunLoop[64726:7390336] -[QLThread dealloc]
可以看到,点了两次创建的两个子线程打印完就挂了。接下来,我们就使用RunLoop来保住它的命看看。
#import "ViewController.h"
#import "QLThread.h"
@interface ViewController ()
@property (nonatomic, strong) QLThread *thread;
@property (nonatomic, assign, getter=isStoped) BOOL stoped;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
//开启一个子线程, 并运行RunLoop
self.thread = [[QLThread alloc] initWithBlock:^{
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
while (weakSelf && !weakSelf.isStoped) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
}];
[self.thread start];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
if (self.thread) {
[self performSelector:@selector(printThread) onThread:self.thread withObject:nil waitUntilDone:NO];
}
}
// 子线程需要执行的任务
- (void)printThread {
NSLog(@"%s thread:%@",__func__, [NSThread currentThread]);
}
// 在子线程停止当前线程的RunLoop
- (void)stop {
if (!self.thread) return;
[self performSelector:@selector(stopRunLoop) onThread:self.thread withObject:nil waitUntilDone:YES];
}
// 停止RunLoop
- (void)stopRunLoop {
self.stoped = YES;
CFRunLoopStop(CFRunLoopGetCurrent());
self.thread = nil;
}
// 保活线程
- (void)hold {
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}
- (void)dealloc {
[self stop];
}
@end
2019-06-21 23:53:36.232546+0800 RunLoop[71963:7473281] -[ViewController printThread] thread:<QLThread: 0x60000365d7c0>{number = 3, name = (null)}
2019-06-21 23:53:38.163183+0800 RunLoop[71963:7473281] -[ViewController printThread] thread:<QLThread: 0x60000365d7c0>{number = 3, name = (null)}
2019-06-21 23:53:39.309686+0800 RunLoop[71963:7473281] -[ViewController printThread] thread:<QLThread: 0x60000365d7c0>{number = 3, name = (null)}
可以看到,每次点击屏幕执行打印任务的子线程都是那个我们保住的那个子线程,而不是重新创建的子线程。
需要注意的是:创建子线程相关的RunLoop,在子线程中创建即可,并且RunLoop中至少要有一个Timer或一个Source保证RunLoop不会因为空转而退出,因此在创建的时候直接加入,如果没有加入Timer或者Source,或者只加入一个监听者,运行程序会崩溃。
2、使用 RunLoop 解决NSTimer在滑动时停止工作的问题
RunLoop有几种模式, iOS中的定时器是在KCFRunLoopDefaultMode(默认模式)下工作的, 当我们在UIScrollView上滑动时, RunLoop就会切换成UITrackingRunLoopMode(界面跟踪模式), 这个时候定时器就会停止工作.
解决这个问题, 我们只要把定时器添加到这两种模式就可以了, 这样定时器在这两种模式下都可以工作了.
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];
3、使用 RunLoop 监控卡顿
导致卡顿的几种原因:
- 复杂 UI、图文混排的绘制量过大
- 在主线程上做网络同步请求
- 在主线程做大量的 IO 操作
- 运算量过大, CPU 持续高占用
- 死锁和主子线程抢锁
对于 iOS 开发来说, 监控卡顿就是要去找到主线程上都做了哪些事. 线程的消息事件是依赖 RunLoop 的, 所以从 RunLoop 入手, 就可以知道主线程上都调用了哪些方法. 我们通过监听 RunLoop 的状态, 就能够发现调用方法是否执行时间过长, 从而判断出是否会出现卡顿.
如果 RunLoop 的线程, 进入睡眠前方法的执行时间过长而导致无法进入睡眠, 或者线程唤醒后接收消息时间过长而导致无法进入下一步的话, 就可以认为是线程受阻了. 如果这个线程是主线程的话, 表现出来的就是出现了卡顿.
我们利用 RunLoop 监控卡顿, 就是要关注这两个阶段. RunLoop在进入睡眠前和唤醒后的两个 loop 状态定义的值, 分别是 kCFRunLoopBeforeSource 和 kCFRunLoopAfterWaiting, 也就是触发 Source0 回调和接收 mach_port 消息两个状态.
4、自动释放池
Timer和Souce也是一些变量,需要占用一部分内存,所以要释放掉,如果不释放掉,就会一直积累,占用的内存就会越来越大。那么这些什么时候释放,怎么释放呢?
RunLoop内部有一个自动释放池,当RunLoop开启时,就会自动创建一个自动释放池,当RunLoop在休息之前会释放掉自动释放池的东西,然后重新创建一个新的空的自动释放池。
最后记得再回顾一下开胃面试题
持续优化中...