1、什么是Runloop
runloop : 运行循环。
一般来说,一个线程一次只能执行一个任务,当执行完任务后线程就会退出,所以需要有个机制保持程序可以随时处理事。
RunLoop可以管理事件/消息,让线程在没有消息处理时休眠以免占用资源,在消息到来时立刻被唤醒。
所以,Runloop实际上是一个对象,管理了其需要处理的事件/消息。
它的作用就是:
保持程序的持续运行
处理App中的各种事件(比如触摸事件、定时器事件等)
节省CPU资源,提高程序性能:该做事时做事,该休息时休息
在iOS中有2套API来访问和使用RunLoop:
- Foundation:NSRunLoop
- Core Foundation:CFRunLoopRef
NSRunLoop和CFRunLoopRef都代表着RunLoop对象
NSRunLoop是基于CFRunLoopRef的一层OC包装
CFRunLoopRef是开源的,源码下载地址:https://opensource.apple.com/tarballs/CF/
2、RunLoop和线程的关系
下面是根据当前线程pthread获取对应runloop的源码,从中我们可以看出:
- 每条线程都有唯一的一个与之对应的RunLoop对象
- RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
- 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
- RunLoop会在线程结束时销毁
- 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop
//用于存放RunLoops的全局Dictionary,线程作为key,RunLoop作为value
static CFMutableDictionaryRef __CFRunLoops = NULL;
// 访问__CFRunLoops(全局Dictionary)时的锁
static CFLock_t loopsLock = CFLockInit;
//获取一个pthread对应的RunLoop
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {//传入了一个线程作为参数,获取该线程对应的runloop
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
if (!__CFRunLoops) { //第一次进入时,初始化全局Dic,并先为主线程创建一个RunLoop
__CFUnlock(&loopsLock);
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
//为主线程创建runloop ---> mainLoop
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);
}
//__CFRunLoops是一个字典,pthreadPointer(t)为一个key值,会根据传入的参数去获取该线程对应的runloop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
if (!loop) {
//如果不存在的话,会调用__CFRunLoopCreate创建一个runloop
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
__CFUnlock(&loopsLock);
CFRelease(newLoop);
}
if (pthread_equal(t, pthread_self())) {
//注册一个回调,当线程销毁时,顺便也销毁其对应的RunLoop
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}
3、RunLoop相关的类
因为NSRunLoop是基于CFRunLoopRef进行包装的,且CFRunLoopRef是开源的,所以研究RunLoop内部的时候,我们看的是CFRunLoopRef。
Core Foundation中关于RunLoop的5个类:
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef
CFRunLoopRef 的内部结构
typedef struct __CFRunLoop * CFRunLoopRef;
struct __CFRunLoop {
// ... more ...
pthread_t _pthread; //线程
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode; //当前模式
CFMutableSetRef _modes; //模式集合,在模式集合中有且只有一个模式为currentMode
// ... more ...
};
CFRunLoopModeRef 的内部结构
struct __CFRunLoopMode {
// ... more ...
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set集合
CFMutableSetRef _sources1; // Set集合
CFMutableArrayRef _observers; //Array 数组
CFMutableArrayRef _timers; //Array 数组
// ... more ...
};
其中CFRunLoopModeRef 和 CFRunLoopRef 的关系:
- 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会立马退出;
图示如下:
CFRunLoopModeRef的模式:
苹果公开提供两个Mode:
kCFRunLoopDefaultMode (NSDefaultRunLoopMode)和 UITrackingRunLoopMode。
其中,kCFRunLoopDefaultMode是App的默认Mode,通常主线程是在该模式下运行的。UITrackingRunLoopMode为页面跟踪Mode,在页面滚动时,该模式可以保证页面滚动不受影响,不会造成卡顿。
另外,苹果还提供了一个操作 Common 标记的字符串:kCFRunLoopCommonModes (NSRunLoopCommonModes),你可以用这个字符串来操作 Common Items,或标记一个 Mode 为 “Common”。这样,当加入到commonModes中时,实际上系统是找出commonModes代表的所有Mode,如defaultMode和trackingMode,然后分别将其加入了这些mode中。例如我们希望定时器在页面滚动的时候不会收到影响,我们会选择kCFRunLoopCommonModes。
CFRunLoopSourceRef
CFRunLoopSourceRef 是事件产生的地方。Source有两个版本:Source0 和 Source1。
Source0主要管理触摸事件、子线程事件处理等。
Source1 被用于管理系统事件捕获、基于Port的线程间通信等;
CFRunLoopTimerRef
CFRunLoopTimerRef 是基于时间的触发器。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
CFRunLoopObserverRef
CFRunLoopObserverRef 是观察者,每个 Observer 都包含了一个回调(函数指针),当 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
};
4、RunLoop的运行逻辑
RunLoop的运行逻辑大致如下图:下面是CFRunLoopRef中的部分源码,从中我们可以看到一个大致的执行流程,因为代码太杂乱,所以经过删除简化:
函数从CFRunLoopRunSpecific中进入,但核心流程在CFRunLoopRun中:
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
__CFRunLoopLock(rl);
//根据传入的modeName找到对应mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
Boolean did = false;
if (currentMode) __CFRunLoopModeUnlock(currentMode);
__CFRunLoopUnlock(rl);
return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
}
volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
CFRunLoopModeRef previousMode = rl->_currentMode;
rl->_currentMode = currentMode;
int32_t result = kCFRunLoopRunFinished;
//通知 Observers : 进入loop循环,而且指定了模式
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
//具体要做的事情,并返回结果
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
//通知 Observers : 退出loop循环
if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
__CFRunLoopModeUnlock(currentMode);
__CFRunLoopPopPerRunData(rl, previousPerRun);
rl->_currentMode = previousMode;
__CFRunLoopUnlock(rl);
return result;
}
CFRunLoopRun函数的代码(简化过):主要是一个do-while循环
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
Boolean didDispatchPortLastTime = true;
int32_t retVal = 0;
//这里的do-while循环,当retVal != 0时才会退出循环
do {
//通知Observers:即将处理timers
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
//通知Observers: 即将处理sources
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
//执行被加入的blocks
__CFRunLoopDoBlocks(rl, rlm);
//处理Sources0(非port) 回调
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
if (sourceHandledThisLoop) {
//返回结果YES的话,再一次处理blocks
__CFRunLoopDoBlocks(rl, rlm);
}
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
msg = (mach_msg_header_t *)msg_buffer;
// 判断有无Source1
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
// 有 Source1 (基于port) 处于 ready 状态的话,跳转至handle_msg
goto handle_msg;
}
}
didDispatchPortLastTime = false;
// 通知 Observers: 即将进入休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
//开始休眠
__CFRunLoopSetSleeping(rl);
__CFPortSetInsert(dispatchPort, waitSet);
__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);
CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent();
do {
// 内部还有一个do-while循环,线程进入休眠,直到被下列事件唤醒,被唤醒后才继续往下执行:
// 1>一个基于 port 的Source 的事件
// 2> 一个 Timer 到时间了
// 3> RunLoop 自身的超时时间到了
// 4> 被其他什么调用者手动唤醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
// Drain the internal queue. If one of the callout blocks sets the timerFired flag, break out and service the timer.
while (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue));
if (rlm->_timerFired) {
// Leave livePort as the queue port, and service timers below
rlm->_timerFired = false;
break;
} else {
if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
}
} else {
// Go ahead and leave the inner loop.
break;
}
} while (1);
__CFRunLoopUnsetSleeping(rl);
// 通知 Observers: 结束休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
// 收到消息,处理消息
handle_msg:;
if (被timer唤醒) {
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
} else if (被gcd唤醒) {//livePort == dispatchPort:看到dispatchPort,多和gcd相关
//处理gcd相关的事情
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else { //被sources1唤醒
//处理sources1
__CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
}
//再次处理blocks
__CFRunLoopDoBlocks(rl, rlm);
//设置返回值,根据返回值决定下一步要做什么,是否退出loop
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休眠实现的原理:
当RunLoo将要进入休眠时,会从用户态切入到内核态,进入内核层面的休眠并等待消息。直到有消息唤醒时,才又从内核态切入到用户态去处理消息。RunLoop在不同状态之间的切换是mach_msg()去发送消息,从而完成状态切换的。
5、RunLoop的应用
- <1>定时器的使用
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"定时器的使用");
}];
// [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
//定时器加到NSRunLoopCommonModes,这样页面scrollview在滑动的时候,定时器依然会执行
NSRunLoopCommonModes并不是一个真实存在的模式,他只是一个标记,传入NSRunLoopCommonModes意味着这个timer在设置了common标记的模式下都可以运行。
如果从结构上来说的话:
__CFRunLoop结构体中有一项叫做CommonModes,该集合中装的是标记了common的模式,默认情况下NSDefaultRunLoopMode、UITrackingRunLoopMode都装在CommonModes中。
当将timer加入到NSRunLoopCommonModes时,timer被加入到__CFRunLoop结构体的_commonModeItems这一集合,这个timer就可以在多个mode中都能执行了。
- <2>控制线程生命周期
看下面的代码:
@interface ViewController ()
@property(nonatomic,strong)NSThread *thread;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(startThread) object:nil];
[self.thread start];
}
-(void)startThread{
NSLog(@"开启子线程");
NSLog(@"子线程结束");
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
[self performSelector:@selector(run) onThread:self.thread withObject:nil waitUntilDone:YES];
}
-(void)run{
NSLog(@"当前线程 %@",[NSThread currentThread]);
}
@end
上面的代码,在启动的时候创建了子线程self.thread,每次点击屏幕的时候在touchesBegan中调用run方法。但是子线程在执行startThread方法后便结束了,所以后续在self. thread上调用run方法会报错,因为self.thread已经销毁了。
所以我们需要用RunLoop来对线程进行保活操作:
@interface ViewController ()
@property(nonatomic,strong)NSThread *thread;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(startThread) object:nil];
[self.thread start];
}
-(void)startThread{
NSLog(@"开启子线程");
//线程保活:往RunLoop里面添加Source\Timer\Observer
//加上runloop之后,不会打印“子线程结束”,而是一直在runloop里面循环、休眠,当点击屏幕会唤醒runloop去处理
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
NSLog(@"子线程结束");
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
[self performSelector:@selector(run) onThread:self.thread withObject:nil waitUntilDone:YES];
}
-(void)run{
NSLog(@"当前线程 %@",[NSThread currentThread]);
}
@end
但是这样还是有不完善的地方,当页面销毁的时候,由于子线程的runLoop在循环运行,倒是self.thread不会销毁,从而导致ViewController也不会被销毁。
下面这种写法更加完善:
@interface ViewController ()
@property(nonatomic,strong)MJThread *thread;
@property (assign, nonatomic, getter=isStoped) BOOL stopped;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//但是这样会有一个强引用,线程self.thread强引用ViewController,ViewController强引用self.thread
//导致页面销毁时,self.thread和ViewController不会被释放掉
//self.thread = [[MJThread alloc] initWithTarget:self selector:@selector(startThread) object:nil];
//[self.thread start];
//使用block,ViewController能够被释放掉,但是线程self.thread不会被释放
//因为runloop一直循环运行,没有被销毁,所以self.thread没有被释放
//所以要设置标记,在合适的时候停止runloop
__weak typeof(self) weakSelf = self;
self.stopped = NO;
self.thread = [[MJThread alloc] initWithBlock:^{
NSLog(@"begin---thread");
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
while (weakSelf && !weakSelf.isStoped ) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
NSLog(@"end---thread");
}];
[self.thread start];
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
if (!self.thread) return;
[self performSelector:@selector(run) onThread:self.thread withObject:nil waitUntilDone:NO];
}
-(void)run{
NSLog(@"当前线程 %@",[NSThread currentThread]);
}
//停止子线程self.thread的runloop
-(void)stopThread{
self.stopped = YES;// 设置标记为YES
CFRunLoopStop(CFRunLoopGetCurrent()); // 停止RunLoop
NSLog(@"stopThread---%@",[NSThread currentThread]);
self.thread = nil;
}
-(void)dealloc{
NSLog(@"%s",__func__);
//waitUntilDone设置成NO,这样可以保证stopThread里面的方法执行完毕,才会这句代码下面的内容,k也就是彻底销毁ViewController
[self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];
}
@end
6、总结
<1>什么是RunLoop,它是怎么做到有事做事,没事休息的?
Runloop是通过内部维护的事件循环来对事件/消息进行管理的一个对象。
当调用CFRunLoopRun()后,处理完毕事件要进入休眠时,系统会调用mach_msg()从用户态进入内核态,从而减少内存开支。当有事情唤醒runloop时,又从内核态切换到用户态。<2>runloop与线程是什么样的关系?
runloop与线程是一一对应的关系
一个线程默认是没有runloop的(主线程默认创建了runloop),我们需要为它手动创建runloop。<3>如何实现一个常驻线程?
(1)创建一个线程对应的runloop
(2)在runloop中添加一个timer、observer、port等内容
(3)调用runloop的run方法<4>当一个处于休眠状态的runloop,有哪些方式可以唤醒它?
source1事件
timer到时间了
runloop自身的超时时间到了
被外部手动操作事件唤醒