一.RunLoop介绍
-
1.概念
RunLoop是一个运行循环,正是因为RunLoop,IOS才可以保持程序的持续运行,处理App中的各种事件,并且可以节省CPU资源,提高性能(因为RunLoop可以做到工作休息两不误)。因为一般来讲,一个线程在处理完一个任务以后就会退出。
线程与RunLoop
- 每条线程都有唯一的一个与之对应的RunLoop对象
- 主线程的RunLoop已经自动创建好了,子线程的RunLoop需要通过系统提供的方法进行获取
- 获取RunLoop以后,如果没有事件源和Timer事件或者没有设置RunLoop运行模式,RunLoop会在获取以后立即销毁;如果超时,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 {
return 0;
}
}
App在启动以后,就会结束运行。
-
2.如何访问RunLoop对象
-
Foundation
NSRunLoop
[NSRunLoop currentRunLoop];//获取当前线程RunLoop,如果当前线程RunLoop未创建,则创建。
[NSRunLoop mainRunLoop];//获取主线程RunLoop
-
Core Foundation
CFRunLoopRef
CFRunLoopGetCurrent();//获取当前线程RunLoop,如果当前线程RunLoop未创建,则创建。
CFRunLoopGetMain();//获取主线程RunLoop
NSRunLoop是CFRunLoopRef的OC封装
-
3.RunLoop相关类介绍
- CFRunLoopRef【RunLoop对象】
- CFRunLoopModeRef【RunLoop的运行模式】
- CFRunLoopSourceRef【RunLoop要处理的事件源】
- CFRunLoopTimerRef【Timer事件】
- CFRunLoopObserverRef【RunLoop观察者】
一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
-
CFRunLoopModeRef
CFRunLoopModeRef代表RunLoop的运行模式(下面列举5种)
NSDefaultRunLoopMode(Cocoa)/kCFRunLoopDefaultMode(Core Foundation):App的默认Mode,通常主线程是在这个Mode下运行
UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
NSRunLoopCommonModes(Cocoa)/kCFRunLoopCommonModes(Core Foundation): 这是一个占位用的Mode,不是一种真正的Mode
- 一个 RunLoop 包含若干个 Mode,每个Mode又包含若干个Source/Timer/Observer
- 每次RunLoop启动时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode
- 如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入
这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响- kCFRunLoopCommonModes是一种模式组合,IOS系统中默认包含了kCFRunLoopDefaultMode和UITrackingRunLoopMode,系统会分别注册这两种模式,还可以通过CFRunLoopAddCommonMode()将自定义Mode放到kCFRunLoopCommonModes中。
-
CFRunLoopSourceRef
-
按照官方文档的分类
Port-Based Sources (基于端口,跟其他线程交互,通过内核发布的消息)
Custom Input Sources (自定义)
Cocoa Perform Selector Sources (performSelector…方法) -
按照函数调用栈的分类
Source0:非基于Port的
Source1:基于Port的
Source0: event事件,只含有回调,需要先调用CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop。
Source1: 包含了一个 mach_port 和一个回调,被用于通过内核和其他线程相互发送消息,能主动唤醒 RunLoop 的线程。
-
CFRunLoopTimerRef
CFRunLoopTimerRef是基于时间的触发器
基本上说的就是NSTimer(CADisplayLink也是加到RunLoop),它受RunLoop的Mode影响
GCD的定时器不受RunLoop的Mode影响
-
CFRunLoopObserverRef
CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变
可以监听的RunLoop状态
/* Run Loop Observer Activities */
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
kCFRunLoopAllActivities = 0x0FFFFFFFU//以上所有状态
};
监听RunLoop状态示例:
//新建子线程
- (void)viewDidLoad {
[super viewDidLoad];
//新建子线程
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(subThreadTask) object:nil];
[self.thread start];
}
- (void)subThreadTask {
//创建观察者
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
{
NSLog(@"即将进入RunLoop");
}
break;
case kCFRunLoopBeforeTimers:
{
NSLog(@"即将处理Timer");
}
break;
case kCFRunLoopBeforeSources:
{
NSLog(@"即将处理Source");
}
break;
case kCFRunLoopBeforeWaiting:
{
NSLog(@"即将进入休眠");
}
break;
case kCFRunLoopAfterWaiting:
{
NSLog(@"即将从休眠中唤醒");
}
break;
case kCFRunLoopExit:
{
NSLog(@"即将退出RunLoop");
}
break;
default:
break;
}
});
//添加观察者
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
//释放资源
CFRelease(observer);
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
self.timer = [NSTimer timerWithTimeInterval:2.0f target:self selector:@selector(timerActon) userInfo:nil repeats:YES];
[runLoop addTimer:self.timer forMode:NSDefaultRunLoopMode];
[runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:4.0f]];
}
- (void)timerActon {
NSLog(@"定时器工作了");
}
运行程序控制台输出:
2018-05-24 09:32:04.495445+0800 Test[1059:57057] 即将进入RunLoop
2018-05-24 09:32:04.499081+0800 Test[1059:57057] 即将处理Timer
2018-05-24 09:32:04.502540+0800 Test[1059:57057] 即将处理Source
2018-05-24 09:32:04.503239+0800 Test[1059:57057] 即将进入休眠
2018-05-24 09:32:06.497401+0800 Test[1059:57057] 即将从休眠中唤醒
2018-05-24 09:32:06.497973+0800 Test[1059:57057] 定时器工作了
2018-05-24 09:32:06.498469+0800 Test[1059:57057] 即将处理Timer
2018-05-24 09:32:06.498736+0800 Test[1059:57057] 即将处理Source
2018-05-24 09:32:06.499872+0800 Test[1059:57057] 即将进入休眠
2018-05-24 09:32:08.500044+0800 Test[1059:57057] 即将从休眠中唤醒
2018-05-24 09:32:08.500392+0800 Test[1059:57057] 定时器工作了
2018-05-24 09:32:08.500790+0800 Test[1059:57057] 即将退出RunLoop
注:因为定时器是每两秒钟调用一次,子线程的runLoop在4秒钟以后会销毁,所以定时器会输出两次。4秒钟以后,runLoop会销毁,在销毁以前观察者会收到"即将推出RunLoop"的状态通知。
二.RunLoop流程
-
1.官方文档
-
2.网友对官方文档的整理
三.RunLoop应用
-
1.NSTimer
创建一个tableView和一个定时器,定时器用于显示当前时间。当tableView静止的时候,定时器正常工作。当拖拽tableView的时候,定时器停止了工作。
定时器的创建代码:
//创建定时器 并指定RunLoop运行模式为NSDefaultRunLoopMode
self.timer = [NSTimer timerWithTimeInterval:1.0f target:self selector:@selector(refreshContentLabel) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
如果需要在列表滑动时定时器继续工作,则需要指定RunLoop运行模式为NSRunLoopCommonModes
//创建定时器 并指定RunLoop运行模式为NSRunLoopCommonModes
self.timer = [NSTimer timerWithTimeInterval:1.0f target:self selector:@selector(refreshContentLabel) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
-
2.PerformSelector
PerformSelector 可以指定在何种RunLoopMode下进行工作
//testAction仅在当前RunLoop处于UITrackingRunLoopMode下工作,即ScrollView滚动的时候
[self performSelector:@selector(testAction) withObject:nil afterDelay:2.0f inModes:@[UITrackingRunLoopMode]];
-
3.常驻线程
有时候我们需要让一个线程保活,即一直处于可以执行任务的状态。这时候我们就需要用到RunLoop。
一般情况,一个线程在任务执行完毕以后,是不可以去执行其他工作的:
- (IBAction)openSubThreadAndexecutingMethodA:(id)sender {
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(methodA) object:nil];
[self.thread start];
}
- (IBAction)executingMethodB:(id)sender {
[self performSelector:@selector(methodAB) onThread:self.thread withObject:nil waitUntilDone:YES];
}
- (void)methodA {
NSLog(@"方法A被执行");
}
- (void)methodAB {
NSLog(@"方法B被执行");
}
当我们点击"开启子线程并执行MethodA"进行子线程创建,并且让MethodA在子线程中执行。当MethodA执行完毕以后,我们如果点击
“让子线程去执行MethodB”按钮,让MethodB在之前创建的子线程中去执行的话,程序就会崩溃。因为当MethodA被执行完毕以后,子线程已经处于finished状态,系统会将其释放。
解决上述问题,让子线程进行常驻,随时可以执行任务:
将上述methodA方法改为:
- (void)methodA {
//在当前子线程中开启一个RunLoop
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
NSLog(@"方法A被执行");
}
此时我们点击"开启子线程并执行MethodA"创建子线程,并且执行methodA。methodA方法是不会输出"方法A被执行"的。因为我们创建了一个持续运行的RunLoop。只有RunLoop结束的时候,此语句才会被输出。我们点击“让子线程去执行MethodB”按钮去执行methodB,程序正常运行。并且我们如果获取此子线程的状态,它处于正在运行的状态,这就达到了保活的目的。在开启RunLoop的时候,我们需要添加事件源或者Timer,否则RunLoop在获取以后,会立马运行结束。
- 4.自动释放池
第一次创建的时机:即将进入runloop的时候【kCFRunLoopEntry】。
释放的时机:runloop进入休眠状态【kCFRunLoopBeforeWaiting】,或者退出runLoop时【kCFRunLoopExit】。
- 注: 当runloop即将休眠的时候会把之前的自动释放池释放,然后重新创建一个新的释放池。