先由一个例子看一下runloop的作用.猜想一下下面代码会如何工作
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSThread *thread = [[NSThread alloc] initWithBlock:^{
NSLog(@"任务A");
}];
[thread start];
[self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}
- (void)test{
NSLog(@"任务B");
}
运行之后点击控制器,程序会崩溃.
任务A
*** Terminating app due to uncaught exception 'NSDestinationInvalidException', reason: '*** -[ViewController performSelector:onThread:withObject:waitUntilDone:modes:]: target thread exited while waiting for the perform'
可以看到线程在等待执行performSelector的时候,已经退出.这是因为线程并没有一个与之对应的runloop对象,所以线程无法正确的执行任务.如果想要线程正确运行,我们可以做如下修改
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSThread *thread = [[NSThread alloc] initWithBlock:^{
NSLog(@"任务A");
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
beforeDate:[NSDate distantFuture]];
}];
[thread start];
[self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}
需要说明的是:
- 获取/创建
runloop之后,必须要为其添加Timer/Port/Source/Observer,否则runloop会退出,线程也无法正确的执行任务 - 创建
NSThread可以通过initWithBlock或initWithSelector这个参数更多的是做runloop的初始化操作,执行线程的任务,一般放在performSelector中执行.
在iOS中,我们想要调用runloop有两种方式:
CoreFoundation层面:CFRunLoopRefFoundation层面:NSRunLoop,它是CFRunLoopRef的OC封装
查看runloop的源码,可以发现CFRunLoopRef由以下几部分构成
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef
它们之间的关系如下:

runloop在CoreFoundation的表现就是一个CFRunLoopRef对象,它由若干个CFRunLoopModeRef组成.
每个CFRunLoopModeRef又包含一个CFRunLoopSourceRef的集合,CFRunLoopTimerRef类型的数组和CFRunLoopObserverRef类型的数组.
查看源码,CFRunLoopModeRef的数据结构如下
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name; // name
Boolean _stopped; // 是否停止
char _padding[3];
//mode 中最核心的四个元素
CFMutableSetRef _sources0; //source0, 这是一个set
CFMutableSetRef _sources1; // source1, set
CFMutableArrayRef _observers; // observer, 数组类型
CFMutableArrayRef _timers; //timers, 数组类型
CFIndex _observerMask;
...
};
CFRunLoopModeRef代表着Runloop的工作模式,在同一个时间runloop只能选择工作在一个mode,并将该mode设定为currentMode- 如果要切换
mode,必须退出当前loop,再选择一个mode重新进入 - 不同mode下的Source0/Source1/Timer/Observer 是分隔开的,互不影响
- 如果Mode 里没有Source0/Source1/Timer/Observer ,runloop 会立刻退出
开发中默认的工作模式是kCFRunLoopDefaultMode,当scrollView滑动的时候处于UITrackingRunLoopMode.这两个是经常会遇到的runloop工作模式.一个老生常谈的问题是:scrollView滑动时,NSTimer将停止运行,这是因为NSTimer默认是在kCFRunLoopDefaultMode工作的,当前scrollView滑动时runloop会切换到UITrackingRunLoopMode,kCFRunLoopDefaultMode停止工作,所以定时器不会在定时执行方法.
CFRunLoopSourceRef 有Source0和Source1两种.
Source1用来处理基于Port的进程间通信.比如触摸屏幕/点击事件/手势,是由硬件监测,再通过进程间通信传递到我们的应用.所以检测用户输入是一个Source1事件Source0只包含了一个回调(函数指针),它并不能主动触发事件.
CFRunLoopTimerRef 它对应的是Foundation层面的NSTimer
CFRunLoopObserverRef 用来监听runloop的状态,每一次runloop状态变化都会知道到它的观察者.runloop有以下几种状态组成
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), //进入runloop
kCFRunLoopBeforeTimers = (1UL << 1), //处理timers之前
kCFRunLoopBeforeSources = (1UL << 2), //处理source之前
kCFRunLoopBeforeWaiting = (1UL << 5), //代表一个时间段:睡眠之前,等待唤醒的一段时间
kCFRunLoopAfterWaiting = (1UL << 6), //代表一个时间段:唤醒之后,处理事件之前的一段时间
kCFRunLoopExit = (1UL << 7), //退出了runloop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
创建runloop之后,我们可以通过如下代码检测它的状态:
- (void)addRlo{
CFRunLoopObserverRef rlo = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"进入runloop");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"处理timers之前");
break;
case kCFRunLoopBeforeSources:
NSLog(@"处理source之前");
break;
//睡眠之前,等待timer或source唤醒
case kCFRunLoopBeforeWaiting:
NSLog(@"------->睡眠之前,等待唤醒");
break;
//代表一个时间段,runloop被唤醒之后,处理唤醒事件之前的一段时间.
case kCFRunLoopAfterWaiting:
NSLog(@"------->唤醒之后,处理事件之前");
break;
case kCFRunLoopExit:
NSLog(@"退出了runloop");
break;
default:
break;
}
});
CFRunLoopRef rl = CFRunLoopGetCurrent();
CFRunLoopAddObserver(rl, rlo, kCFRunLoopDefaultMode);
CFRelease(rlo);
}
runloop的核心是一个do-while循环,提供了一套让线程有事件的时候处理事件,没有事件的时候休眠的机制.
它提供了一个runloop对象来管理其所需要处理的事件和消息,并且提供了一个run函数,来执行这个do-while循环.
它伪代码实现如下:
-(void)run{
int retVal = 0;
do{
//休眠的同时等待消息
int message = sleep_and_wait();
//接受到消息之后,处理消息
retVal = process_message(message);
}while(ret == retVal)
}
- 保证iOS应用的存活,在
main函数中UIApplicationMain(argc, argv, nil, appDelegateClassName)开启了主线程的runloop,保证了应用不会启动之后立马退出 - 处理App中的各种事件:触摸事件/定时器事件/界面刷新/autoreleasepool 等
- 节省CPU资源,提高程序性能:该做事时做事,该休眠时休眠
-
默认情况下,线程执行完任务就会结束,
runloop的这种do-while机制,提供了一种保住线程的能力. -
子线程要想正常工作,必须创建一个与之对应的
runloop对象: ①要向runloop中添加Source/Timer/Observer,没有这些runloop会立刻退出 ②调用它的run/runMode:beforeDate方法.没有调用,不会启动do-while循环 -
主线程的
runloop是默认创建且开启的 -
每条线程都有一个唯一与之对应的
runloop对象,这种对应关系存在一个全局的字典中,线程作为key,runloop作为value
假如我们需要频繁的在子线程中做事情,但是每次创建线程,销毁线程都会有较大的系统资源开销.这个时候,我们就需要一条常驻线程来实现目的.
实际开发中,实现一个常驻线程是比较容易的,创建runloop/添加Port/调用run方法,即可很快的实现一条常驻线程.但是这条线程如何销毁其实是问题比较大的
具体的实现,可以参考LongThread.提供了Foundation和CoreFoundation的两种实现.在实现过程中发现,有以下细节要注意.
查看定义:
In other words, this method effectively begins an infinite loop that processes data from the run loop’s input sources and timers.
它高效的开启了无限循环的runloop来处理source和timers的输入数据.相当于这是一个死循环,即便你可以通过CFRunLoopStop(CFRunLoopGetCurrent());停掉其中一次runloop,它仍然处在一个while(1)循环中,还是是无法停止的.
while(1){
//runloop
int retVal = 0;
do{
int message = sleep_and_wait();
retVal = process_message(message);
}while(ret == retVal)
}
为了解决这个问题,我们有两个方案可选
- 采用
NSRunloop层面的runMode:beforeDate方法 - 采用
CoreFoundation层面的CFRunLoopRunInMode函数
- (void)stopThread{
CFRunLoopStop(CFRunLoopGetCurrent());
}
- (void)dealloc{
NSLog(@"%s",__func__);
[self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:NO];
}
以上程序在控制器释放的时候会崩溃,因为waitUntilDone:NO这个参数决定了是在子线程异步去关闭runloop,但是在此时可能主线程中控制对象已经释放掉了,如果再在子线程中去访问控制器的属性,是会造成坏访问的.
解决:waitUntilDone参数改为YES
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.stop = NO;
__weak typeof(self) weakSelf = self;
MyThread *thread = [[MyThread alloc]initWithBlock:^{
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
while (!weakSelf.isStopped) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
beforeDate:[NSDate distantFuture]];
}
}];
self.thread = thread;
[thread start];
}
在将waitUntilDone改为YES后,控制器销毁,self=nil,!weakSelf.isStopped为YES,所以仍然不能正确的停止.
故正确的判断逻辑应该是:
while (weakSelf && !weakSelf.isStopped) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
beforeDate:[NSDate distantFuture]];
}
initWithTarget创建的线程,会对控制器有一个强引用,为了避免循环引用我们尽量用initWithBlock方法创建- 假如线程已经
exited,但是线程对象依然处在存活状态,在执行perfomSelector:onThread会崩溃,所以在stopThread应该把线程置位nil
- (void)stopThread{
CFRunLoopStop(CFRunLoopGetCurrent());
self.thread = nil;
}
借用YYKit作者的一张图,先直观的看一下runloop的运行逻辑.

Source0(Port)唤醒runloop应该是原作者笔误,应该是Source1(Port).因为Source0不是基于Port的,Source1才是;另外Source0也不备注主动唤醒runloop的能力
void CFRunLoopRun(void) {
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(),
kCFRunLoopDefaultMode,
1.0e10,
false);
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
//非 kCFRunLoopRunStopped 和 kCFRunLoopRunFinished 一直循环
}
只要runloop的状态不是kCFRunLoopRunStopped和kCFRunLoopRunFinished, runloop就会一直运行.这也就是为什么我们在程序在执行完UIApplicationMain不会挂掉的原因.
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
//1.通知Observer 进入 currentMode,对应上图的第1步
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
//这个地方是runloop真正进入循环的入口,对应图上的第2-9步,其内部也是一个do-while循环
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
//10.通知Observer 退出 currentMode,对应上图的第10步
if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
return result;
}
该函数会通知Observer即将进入runloop和退出runloop,进入runloop后的操作在__CFRunLoopRun中实现,其内部也是do-while循环保证了线程在当前mode下,能够有事做事无事休眠的逻辑.
两层do-while循环的设计是因为:内层的do-while循环在切换mode的时候,会退出当前循环.如果只有一层循环,是无法保证程序一直运行的.
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
Boolean sourceHandledThisLoop = NO;
int retVal = 0;
do {
/// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 4. RunLoop 触发 Source0 (非port) 回调。
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
if (hasMsg) goto handle_msg;
}
/// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}
/// 7. 调用 mach_msg 等待接收消息。线程将进入休眠, 直到被下面某一个事件唤醒。
/// • 一个基于 port 的Source 的事件,如用户点击/触摸灯事件
/// • 一个 Timer 到时间了
/// • RunLoop 自身的超时时间到了
/// • 如果有dispatch到main_queue的block
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
}
/// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
/// 收到消息,处理消息。
handle_msg:
/// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}
/// 9.2 如果有dispatch到main_queue的block,执行block。
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}
/// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
else {
CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
if (sourceHandledThisLoop) {
mach_msg(reply, MACH_SEND_MSG, reply);
}
}
/// 执行加入到Loop的block
__CFRunLoopDoBlocks(runloop, currentMode);
if (sourceHandledThisLoop && stopAfterHandle) {
/// 进入loop时参数说处理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout) {
/// 超出传入参数标记的超时时间了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(runloop)) {
/// 被外部调用者强制停止了
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
/// source/timer/observer一个都没有了
retVal = kCFRunLoopRunFinished;
}
/// 如果没超时,mode里没空,loop也没被停止,那继续loop。
} while (retVal == 0);
}
以上即runloop执行流程图中第2-9步的执行逻辑.对以上关键点做以下解析:
第7步调用mach_msg之后,程序是由用户态进入了内核态,达到线程有事做事,无事休眠的状态.这种状态和sleep(1)是不一样,它会卡着线程,无法处理任何输入输出事件;和while循环也不一样,这会让程序一直循环处理任务,没有达到节省资源的目的.调用mach_msg之后,程序相当于处在一个卡住的状态,后面的代码不会继续执行,直到有输入源唤醒了run loop,执行完唤醒事件,如果runloop没有退出,则继续执行下一次循环.
唤醒runloop的有以下三种类型事件:
- Source1,也即Port通信.例如用户点击/触摸屏幕/手势
- Timer,当定时器的时间达到之后,会唤醒
runloop执行事件 - dispatch_async(dispatch_get_main_queue(), block) 调用.libDispatch会向主线程
runloop发送消息唤醒主线程runloop.libDispatch唤醒runloop仅限主线程,dispatch到其他线程仍由libDispatch处理.
在runloop源码中,可以看到这些do函数内部都调用了一个很长的calling_out函数,这些函数的目的在于将runloop中接受的事件从系统的Runloop层面传递到上层,中间可能会经过一些额外的处理,最终到达程序员所编写的代码层面.
static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();
当我们讨论runloop时,探讨其与线程的关系是最多的.这一方面是因为在创建子线程的时候,必须获取一个对应的runloop对象,另外我们能够方便直观的创建线程来探索两者之间的关系.
其实runloop的事件机制,在App中有更为底层的应用,只不过这些机制被系统很好的隐藏了实现的细节,我们很难一窥究竟.但是我们通过在程序启动之后打印currenMode/符号断点/LLDB的bt命令,查看其中的细节.
runloop 在程序启动的时候,注册一些观察者,这些观察在接收到事件的时候,会在runloop的合适时机出执行这些事件.
添加符号断点:__IOHIDEventSystemClientQueueCallback
在应用启动后,苹果注册了一个Source1用来接收用户输入事件,如触摸屏幕/点击事件/手势等,其回调函数为__IOHIDEventSystemClientQueueCallback.
用户输入-->硬件监测到IOHIDEvent --> mach port 发送消息给App进程 --> 注册的Source1 触发 --> _UIApplicationHandleEventQueue()调用,进行事件分发.
_UIApplicationHandleEventQueue() --> 识别为 UIEvent,如UIButton click、touchesBegin/Move/End/Cancel _UIApplicationHandleEventQueue() --> 识别为UIGestureRecognizer
符号断点:_wrapRunLoopWithAutoreleasePoolHandler
应用启动后,runloop注册了两个Observer,这两个观察者的callback都是_wrapRunLoopWithAutoreleasePoolHandler.
第一个观察者监测的事件是:即将进入runloop(kCFRunLoopEntry),此时会调用objc_autoreleasePoolPush创建自动释放池,这个活动优先级最高,确保在进入runloop的时候,自动释放池已经创建好了.
第二个观察者监测了两个事件:kCFRunLoopBeforeWaiting和kCFRunLoopExit,此时会调用_objc_autoreleasePoolPop() 和_objc_autoreleasePoolPush() 释放旧池创建新池.它的优先级是最低的,确保释放自动池在其他回调之后.
监听了kCFRunLoopBeforeWaiting事件给与自动释放在程序空闲的时候释放内存的能力,即不占用其他回调的处理周期,又可以有效避免出现内存高峰.
符号断点:ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv
应用启动后会注册一个观察者,监听监听 kCFRunLoopBeforeWaiting(即将进入休眠) 和 kCFRunLoopExit (即将退出Loop) 事件,其回调是_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv().
当我们在程序中,修改了Frame/更改了View层级/调用setNeedsLayout/setNeedsDisplay 后,这些动作其实不是被立即执行的,它被提交到一个全局的容器中,当runloop处于即将休眠的时候,会通知该观察者,此时去更新界面层级与布局
一个NSTimer注册到runloop后,会计算好其回调的时间点,到时间会唤醒runloop执行回调事件
Timer有一个属性Tolerance(宽容度),标记了到时候后容许有多大的误差.假如到时间后,正好有事件占用这次loop循环,且执行之后这个时间已经超过了这个宽容度,那么这次Timer事件回调会被跳过.
其内部也会创建一个Timer,并添加到当前线程的runloop中,如果当前线程没有runloop则Timer也会失效.
当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的
上面已经实现 具体参数 LongThread
实现UITableView滑动的时候,不加载图片的方法:
我们知道UITableView滑动是在UITrackingMode,我们只需要把图片的加载放在NSRunLoopDefaultMode即可.即调用以下方法:
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes