前言
文章主要分为四个部分
- 一、RunLoop 简介
- 二、RunLoop 相关接口
- 三、RunLoop 相关逻辑流程
- 四、RunLoop 实际应用
一、RunLoop 简介
1.1 RunLoop 基本概念
一个线程一次只能执行一个任务,执行完成后线程就会退出。RunLoop 机制能让线程随时处理事件但并不退出。这里说的随时是指:程序需要运行时就保持程序的持续运行,不需要的时候就进入休眠状态。
NSRunLoop 和 CFRunLoopRef 都是和RunLoop 机制相关的类。CFRunLoopRef 基于 CoreFoundation 框架内,是纯 C 函数的 API,所有这些 API 都是线程安全的。CFRunLoopRef 的代码是开源的。NSRunLoop 是基于 CFRunLoopRef ,提供了面向对象的 API,但是这些 API 不是线程安全的。
1.2 RunLoop 和 线程的关系
关于RunLoop 和线程之间的关系要知道以下几点:
- 苹果不允许直接创建 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。
- 线程和 RunLoop 是一一对应的,其关系是保存在一个全局的 Dictionary 里。
- 只能在当前线程中操作当前线程的RunLoop,而不能去操作其他线程的RunLoop。
- RunLoop对象在第一次获取RunLoop时创建,销毁则是在线程结束的时候。
- 主线程的RunLoop对象系统自动帮助我们创建好了,而子线程的RunLoop对象需要我们主动获取,因为子线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。
1.3 为什么 main
函数不会 return
掉 ?
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
上面main
函数同一般函数相比,启动程序后并不会立刻 return 掉。其中UIApplicationMain函数内部默认开启了主线程的 RunLoop ,并执行了一段无限循环的代码。UIApplicationMain函数一直没有返回,所以运行程序之后会保持持续运行状态。
//无限循环代码模式
int main(int argc, char * argv[]) {
BOOL running = YES;
do {
// 执行各种任务,处理各种事件
// ......
} while (running);
return 0;
}
二、RunLoop 相关接口
2.1 RunLoop 的结构
和 RunLoop 相关的主要涉及五个类:
- CFRunLoopRef:RunLoop对象
- CFRunLoopModeRef:运行模式
- CFRunLoopSourceRef:输入源/事件源
- CFRunLoopTimerRef:定时源
- CFRunLoopObserverRef:观察者
从上图可以看出,RunLoop 对象中可以包含多个 Mode,每个 Mode 又包含多个个 Source、Timer、Observer。
2.2 RunLoop 中的 Mode
关于Mode首先要知道一个RunLoop 对象中可能包含多个Mode,且每次调用 RunLoop 的主函数时,只能指定其中一个 Mode(CurrentMode)。切换 Mode,需要重新指定一个 Mode 。主要是为了分隔开不同的 Source、Timer、Observer,让它们之间互不影响。
总共是有五种Mode:
-
kCFRunLoopDefaultMode
:默认模式,主线程是在这个运行模式下运行 -
UITrackingRunLoopMode
:跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响) -
UIInitializationRunLoopMode
:在刚启动App时第进入的第一个 Mode,启动完成后就不再使用 -
GSEventReceiveRunLoopMode
:接受系统内部事件,通常用不到 -
kCFRunLoopCommonModes
:伪模式,不是一种真正的运行模式,实际是kCFRunLoopDefaultMode
和UITrackingRunLoopMode
的结合。
有这样一个场景,假设自己封装一个无限轮播视图,很有可能会出现这样一种情况:当你滑动轮播视图时,轮播视图的定时器不再起作用,不能通过定时器调整UIScrollView的偏移值。之所以会出项上述现象,是因为主线程的 RunLoop 里有两个 Mode:kCFRunLoopDefaultMode
和 UITrackingRunLoopMode
。默认情况下是defaultMode
,但是当滑动UIScrollView
时,RunLoop 会将 mode 切换为 TrackingRunLoopMode
,这时 Timer 就不会被回。如果想在滑动的时候不让定时器失效,可以使用CommonMode来解决。
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
2.3 Mode 中的 CFRunLoopSourceRef
CFRunLoopSourceRef是事件源,主要有两种分类方式,一种是苹果官方的分类方式,另一种是按照函数调用栈栈分类方式。
2.3.1 官方分类
- Port-Based Sources(基于端口)
- Custom Input Sources(自定义)
- Cocoa Perform Selector Sources
2.3.2 按照函数调用栈分类
- Source0 :非基于 Port。只包含了一个回调(函数指针),不能主动触发事件。使用时,需先调用
CFRunLoopSourceSignal(source)
,将 Source 标记为待处理,然后手动调用CFRunLoopWakeUp(runloop)
唤醒 RunLoop,让其处理这个事件。触摸事件处理和 performSelector:onThread: 都会触发 Source0 。 - Source1:基于Port,通过内核和其他线程通信,接收、分发系统事件。 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程。基于Port的线程间通信和系统事件捕捉都是 Source1 完成,当 Source1 捕捉到系统时间后,会放在队列中,之后再依次包装为 Source0 处理。
创建一个按钮,添加点击事件,并在按钮回调事件添加断点,当执行到断点出左侧会出现相关栈调用信息。从上图可以看出:点击事件就是在
Sources0
中处理的。至于
Source1
主要是用来接收、分发系统事件,然后再分发到
Sources0
中处理。
2.4 Mode 中的 CFRunLoopTimerRef
CFRunLoopTimerRef
是定时源,你可以简单把它理解为NSTimer
。其包含一个时间点和一个回调(函数指针)。当被加入到 RunLoop 时,RunLoop 会注册对应的时间点,当时间到时,RunLoop 会执行对应时间点的回调。NSTimer 和 performSelector:withObject:afterDelay: 都是通过其处理的。
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
2.5 Mode 中的 CFRunLoopObserverRef
CFRunLoopObserverRef
是观察者,主要用来监听RunLoop 的状态,主要有以下几种状态。
- kCFRunLoopEntry : 即将进入RunLoop
- kCFRunLoopBeforeTimers :即将处理Timer
- kCFRunLoopBeforeSources:即将处理Source
- kCFRunLoopBeforeWaiting :即将进入休眠
- kCFRunLoopAfterWaiting:即将从休眠中唤醒
- kCFRunLoopExit :即将从RunLoop中退出
- kCFRunLoopAllActivities:监听全部状态改变
可以通过以下代码验证RunLoop的几种状态:
// 创建观察者
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"监听到RunLoop发生改变---%zd",activity);
});
// 添加观察者到当前RunLoop中
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// 释放observer
CFRelease(observer);
除了用于监听 RunLoop 的状态之外,UI刷新(BeforeWaiting)、Autorelease pool(BeforeWaiting)都与其有关系。
三、RunLoop 相关逻辑流程
上图是笔者从网上找到的一张 RunLoop 运行的相关流程逻辑图。具体来说主要执行逻辑是这样的:
- 1、通知观察者 RunLoop 已经启动。
- 2、通知观察者即将要开始定时器。
- 3、通知观察者任何即将启动的非基于端口的源。
- 4、启动任何准备好的非基于端口的源(Source0)。
- 5、如果基于端口的源(Source1)准备好并处于等待状态,进入步骤9。
- 6、通知观察者线程进入休眠状态。
- 7、将线程置于休眠状态,知道下面的任一事件发生才唤醒线程。
. 某一事件到达基于端口的源
. 定时器启动。
. RunLoop 设置的时间已经超时。
. RunLoop 被唤醒。 - 8、通知观察者线程将被唤醒。
- 9、处理未处理的事件。
.如果用户定义的定时器启动,处理定时器事件并重启RunLoop。进入步骤2。
.如果输入源启动,传递相应的消息。
.如果RunLoop被显示唤醒而且时间还没超时,重启RunLoop。进入步骤2 - 10、通知观察者RunLoop结束。
四、RunLoop 实际应用
4.1 后台常驻线程
借助RunLoop可以实现线程后台常驻的功能,关键是在于两行代码,具体请看如下代码。
- (void)viewDidLoad {
[super viewDidLoad];
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(runOne) object:nil];
[self.thread start];
}
- (void) runOne{
NSLog(@"----任务1-----");
// 下面两句代码可以实现线程保活
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
// 测试是否开启了RunLoop,如果开启RunLoop,则来不了这里,因为RunLoop开启了循环。
NSLog(@"未开启RunLoop");
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
// 利用performSelector,在self.thread的线程中调用run2方法执行任务
[self performSelector:@selector(runTwo) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void) runTwo{
NSLog(@"----任务2------");
}
实现了上述代码之后,每次点击屏幕都会打印----任务2------,这说明子线程处于活跃状态。如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出,上述代码中的 [NSPort port]
相当于往 RunLoop 中添加 Source1。[[NSRunLoop currentRunLoop] run]
相当于开启了一个无限循环,默认是 defaultMode,相应的线程永远也不会释放。即使调用CFRunLoopStop(CFRunLoopGetCurrent)
也只能停止其中的一次 [[NSRunLoop currentRunLoop] run]
,并不能持续有效。
在一些分析AFNetworking
源码的文章中,也经常会出现如下这些代码。其核心也是为了实现线程后台常驻。
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
当后台线程执行任务时,通过 performSelector:onThread:..
方法将任务放在后台线程的 RunLoop 中。正常来说,一个线程执行完任务后就退出了。开启runloop是为了防止线程退出。一方面避免每次请求都要创建新的线程;另一方面,因为connection 的请求是异步的,如果不开启runloop,线程执行完代码后不会等待网络请求完的回调就退出了,这会导致网络回调的代理方法不执行。
- (void)start {
[self.lock lock];
if ([self isCancelled]) {
[self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
} else if ([self isReady]) {
self.state = AFOperationExecutingState;
[self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
}
[self.lock unlock];
}
4.2 AutoreleasePool
应用程序一旦启动,主线程 RunLoop 里注册了两个 Observer。一个 Observer 监听即将进入 Loop 事件,回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池,并保证创建释放池发生在其他所有回调之前。另外一个 Observer 监视了两个事件 (RunLoop即将进入休眠和即将退出 RunLoop 事件) ,前者会调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;后者会调用 _objc_autoreleasePoolPop() 来释放自动释放池,并保证释放自动释放池事件发生在其它回调之后。
4.3 卡顿监测
所谓的卡顿一般是在主线程做了耗时操作,卡顿监测的主要原理是在主线程的 RunLoop 中添加一个 observer,检测从 即将处理Source(kCFRunLoopBeforeSources)
到 即将进入休眠 (kCFRunLoopBeforeWaiting)
花费的时间是否过长。如果花费的时间大于某一个阙值,则认为卡顿,此时可以输出对应的堆栈调用信息。具体可以参考此篇文章。
补充
RunLoop休眠的实现原理
用户态调用 mach_msg
函数会转而调用内核态的 mach_msg
函数,内核态 mach_msg
函数没有消息就让线程休眠,有消息就唤醒线程回到用户态处理消息。