从字面上来讲是运行循环,也能够翻译为跑圈.安全
RunLoop和线程是息息相关的,咱们都知道线程的做用就是用来执行特定的一个或多个任务,正常状况下,线程执行完当前任务后就会退出,以后若线程又有任务须要执行也没法继续执行了.这时咱们就须要一种方式让线程能不断执行任务,即便当前线程没有任务执行,线程也不会退出,而是等待下一个任务的到来.因此咱们就有了RunLoop.markdown
每一条线程都有惟一一个与之对应的RunLoop对象.app
主线程的RunLoop对象系统已经自动帮咱们建立好了,而且只有主线程结束时即程序结束时才会销毁.框架
子线程的Runloop对象须要咱们主动建立并维护,子线程的Runloop对象在第一次获取时就会建立,销毁则是在子线程结束时. 而且建立出来的runLoop对象默认是不开启的,必须手动开启RunLoop.ide
Runloop并不保证线程安全,咱们只能在当前线程内部操做当前线程的Runloop对象,而不能在当前线程中去操做其余线程的RunLoop对象.函数
相关代码以下:oop
NSRunLoop *currentRunLoop = [NSRunloop currentRunloop] //获取当前线程的RunLoop对象,在子线程中调用时若是是第一次获取内部会帮咱们建立RunLoop对象
[currentRunLoop run];
[NSRunLooop mainRunLoop] //获取主线程的RunLoop对象
复制代码
咱们在启动一个程序时,系统会自动调用建立项目时自动建立的main.m 的文件.main.m文件以下所示:性能
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
复制代码
其中UIApplicationMain
函数中内部帮咱们开启了主线程的RunLoop,这个RunLoop使得程序只要不退出或者崩溃,UIApplicationMain
函数就一直不会返回,保持了程序的持续运行.上边的代码中主线程开启RunLoop的过程能够简单理解为如下代码:spa
int main(int argc, char * argv[]) {
BOOL isRunning = YES;
do {
//执行各类任务,处理各类事件
} while(isRunning);
return 0;
}
复制代码
下图是苹果官方的RunLoop模型图线程
从上图能够看出RunLoop就是线程中的一个循环,RunLoop会在循环中经过 Input sources(输入源) 和 Timer sources(定时源)不断检测是否有事件须要执行.而后对接收到的事件通知线程去处理,而且在没有事件的时候让线程去休息.
iOS为咱们提供了两套API来访问RunLoop, 一套是Foundation框架的NSRunLoop, 一套是Core Foundation框架的CFRunLoop. NSRunloop本质是基于CFRunLoop的oc对象封装,因此咱们在这里就讲解Core Foundation框架下有关RunLoop的五个类.
5 CFRunLoopObserverRef: 观察者,可以监听RunLoop的状态改变
下面详细讲解几种类的具体含义相互关系. 先来看看一张能表示五个类关系的图:
接着来说解这五个类的相互关系:
一个RunLoop对象(CFRunLoopRef)包含若干个运行模式(CFRunLoopModeRef)。而每一个运行模式下又有若干个输入源(CFRunLoopSourceRef),定时源(CFRunLoopTimerRef),观察者(CFRunLoopObserverRef)
下面咱们来详细讲解一下这五个类:
CFRunLoop类是Core Foundation框架下的RunLoop对象类.咱们能够经过如下方式获取RunLoop对象
CFRunLoopGetCurrent(); //获取当前线程的RunLoop对象,在子线程中调用时若是是第一次获取内部会帮咱们建立RunLoop对象
CFRunLoopGetMain(); //获取主线程的RunLoop对象
系统默认定义了多种运行模式, 以下:
其中kCFRunLoopDefaultMode, UITrackingRunLoopMode,kCFRunLoopCommonModes是咱们开发中须要用到的模式.具体使用方法咱们在2.3 CFRunLoopTimerRef中结合CFRunLoopTimerRef来演示说明
CFRunLoopTimerRef是定时源, 理解为基于时间的触发器, 基本上就是NSTimer. 下面咱们来演示一下CFRunLoopModeRef和CFRunLoopTimerRef结合的使用方法.
在Main.Storyboard中拖入一个textView. 而后尝试执行如下代码:
- (void)viewDidLoad {
[super viewDidLoad];
[self timer1];
}
- (void)timer1 {
//1.建立定时器
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
//2.将定时器添加到当前的RunLoop,指定RunLoop的运行模式为默认运行模式
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
}
- (void)run {
NSLog(@"run --- %@ --- %@", [NSThread currentThread], [NSRunLoop currentRunLoop]);
}
复制代码
当程序运行时, run方法每隔两秒就会执行一次, 可是若拖动textView,run方法就不会执行.这是由于什么呢?
咱们建立的timer是加入到RunLoop的NSDefaultRunLoopMode运行模式中, 可是当咱们拖动textView,当前RunLoop会退出当前运行模式,并进入到UITrackingRunLoopMode运行模式,咱们建立的timer并无添加到并到UITrackingRunLoopMode运行模式中,因此run方法就不会执行.
那么有什么解决方法呢?
解决方法一:
把timer也添加到UITrackingRunLoopMode运行模式中.这样就能够在两种运行模式下都执行run方法.
增长代码以下:
[[NSRunLoop currentRunLoop]addTimer:timer forMode:UITrackingRunLoopMode];
复制代码
解决方法二:
把timer加入到kCFRunLoopCommonMode运行模式中.前面2.2中已经提到这种模式其实知识一种占位模式,并非真正的运行模式.如果将timer添加到这个模式中,那么timer会被添加到打上common标签的运行模式中.
那么那些运行模式会被打上common标签呢?
NSDefaultRunLoopMode 和 UITrackingRunLoopMode
因此只要添加到kCFRunLoopCommonMode运行模式也就等价于把timer加入到NSDefaultRunLoopMode和UITrackingRunLoopMode这两种运行模式中.
将代码替换成以下代码:
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
复制代码
除了上面代码中使用的timer的建立方法,还有一种经常使用的timer建立方法
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
复制代码
这种方法建立出来的timer会被默认添加到NSDefaultRunLoopMode运行模式,若想添加到UITrackingRunLoopMode中,只要拿到timer对象而后选择上面的其中一种解决方法便可.
刚才提到了例子都是在主线程中建立timer并加入到RunLoop中特定的运行模式中,那么要是在子线程中建立timer有什么区别呢?
请尝试执行下面的代码:
- (void)viewDidLoad {
[super viewDidLoad];
[NSThread detachNewThreadSelector:@selector(timer2) toTarget:self withObject:nil];
}
- (void)timer2 {
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
}
- (void)run {
NSLog(@"run --- %@ --- %@", [NSThread currentThread], [NSRunLoop currentRunLoop]);
}
复制代码
你会发现run方法根本不会调用,这是为何呢?
这其实就要和上面提到的runLoop的的建立和管理有关了.
子线程的Runloop对象须要咱们主动建立并维护,子线程的Runloop对象在第一次获取时就会建立,销毁则是在子线程结束时. 而且建立出来的runLoop对象默认是不开启的,必须手动开启RunLoop.
因此咱们应该修改代码为以下:
- (void)timer2 {
//1.获取RunLoop并建立
NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
//2.建立timer
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
//3.启动子线程的RunLoop
[currentRunLoop run];
}
复制代码
CFRunLoopSourceRef是事件源(RunLoop模型图中提到过的)
第一种是经过官方理论来分的, 第二种是在实际应用中经过调用函数来分的.
下面咱们举个例子经过函数调用栈中的source
1.首先咱们在main.storyboard中拖入一个按钮,并添加动做
2.而后在点击动做中的代码中加入一个输出语句,并打上一个断点
步骤以下:
当咱们运行程序后点击按钮后就会来到此断点,而后咱们就能够查看当前的函数调用栈.
以下图所示:
因此点击事件是这样来的:
CFRunLoopObserver是监听者, 可以监听RunLoop的状态改变.
能够监听的时间点有如下:
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 //监听全部事件
};
复制代码
具体使用方法以下:
- (void)viewDidLoad {
[super viewDidLoad];
[self observer];
}
- (void)observer {
/**
@param1:怎么分配空间(通常传入默认分配方式)
@param2:要监听的RunLoop的什么状态
@param3:是否要持续监听
@param4:优先级 老是传0
@param5:当状态改变时的回调
*/
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;
}
});
/**
@param1:要监听的RunLoop对象
@param2:观察者
@param3:运行模式
*/
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
}
复制代码
打印台信息以下:
能够看到RunLoop在程序运行后就会处理大量的Source和Timer事件,当没有事情须要作的时候就会进入休眠状态,即让线程休眠,当有事件须要处理时就会唤醒RunLoop再次处理事件,
五个类都理解完以后咱们就来具体说明RunLoop的运行原理.
其中咱们借助下面这张网友的逻辑图进行说明
结合上面这个逻辑图咱们来讲明一个苹果官方文档给出的RunLoop运行逻辑
具体顺序以下:
首先RunLoop会去检查Mode里是否有source/timer, 没有直接退出
前面都是一些理论知识的讲解,接下来咱们咱们就讲讲在实战中如何使用RunLoop.
刚刚在前面的2.3中咱们已经讲解了把Timer加入到RunLoop的不一样运行模式的做用和区别.你们若是忘了能够回去再看看如何使用.
咱们可能有时会遇到一种状况,就是咱们的界面有tableView,每一个tableView的cell中都有许多图片.而后当咱们滚动tableView,须要显示不少图片,这时候可能就会出现卡顿现象.
那么这时咱们就可使用RunLoop来解决这个问题.具体方法为利用performSelector
方法调用UIImageView的setImage:
方法,而后指定在RunLoop下的NSDefaultRunLoopMode运行模式.代码以下:
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"男孩"] afterDelay:5.0 inModes:@[NSDefaultRunLoopMode]];
复制代码
咱们设置显示图片的时间为五秒以后,可是程序运行后咱们拖动textView,发现五秒后图片并无出现,而是当咱们拖动结束时候才显示出来.
这是由于咱们设置显示图片的操做是在RunLoop的NSDefaultRunLoopMode模式中,当咱们拖动textView时,RunLoop会切换到UITrackingRunLoopMode模式,这时即便设定的操做执行时间也不会执行,而是要等到咱们结束完拖动后才会切换回NSDefaultRunLoopMode模式执行设置图片的操做.
在上面推迟显示图片的程序中,咱们能够发现当咱们切换到UITrackingRunLoopMode中,设定的执行操做的时间并无中止计时,因此当咱们一中止拖动时就会立刻执行操做.
那么咱们要是在RunLoop的NSDefaultRunLoopMode模式下添加了一个timer,拖动textView一段时间后,许多本该执行的操做在中止拖动以后会怎样执行呢.让咱们运行下面代码来看看效果吧.
- (void)viewDidLoad {
[super viewDidLoad];
//[self observer];
NSLog(@"%s", __func__);
[self test2];
}
- (void)test2 {
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"test2");
}];
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
}
复制代码
效果以下图:
我是在13:02:50进行拖动textView,而后13:03:14结束拖动,能够发现若是当时期间24秒间隔本应该要执行12次打印,最后只执行了两次,并且这两次执行是基本紧接着执行的,期间没有间隔.而后又开始了正常的两秒种执行一次打印.
因此咱们能够得出RunLoop的逻辑,当timer添加到RunLoop的NSDefaultRunLoopMode模式时,在切换到UITrackingRunLoopMode模式后,RunLoop会最多暂存两次操做,而后等到RunLoop切换回NSDefaultRunLoopMode模式下,再紧挨着执行两次操做.
结论:
因此当NSTimer添加到NSDefaultRunLoopMode模式并非绝对精准的,当咱们滚动一些视图时,执行操做就会变得不按时.解决方法就是把timer也添加到UITrackingRunLoopMode模式中,或者使用其余定时器如GCD定时器.
[NSThread detachNewThreadSelector:@selector(run1) toTarget:self withObject:nil]
会建立并自动开启一条线程执行任务,不须要手动启动
咱们以前建立线程都是为了执行特定任务,执行问特定任务后,线程会自动进入死亡状态.线程进入死亡状态后,是没法再次启动线程,让线程继续执行任务的.
若线程进入死亡状态再次调用start方法会报错
咱们在作项目时可能会在后台执行频繁操做,在子线程中执行耗时操做(以下载文件,后台播放音乐,后台记录用户信息),那么我最好能让线程不进入死亡状态所以能够持续的执行任务,而不是频繁的建立和销毁线程.
那么咱们应该怎么作呢?
添加一条指向常驻内存的线程强引用,而后在这条线程中建立一个RunLoop,并添加一个Sources,而后开启RunLoop.缘由是RunLoop只要没有超时,任务就会一直执行不完,那么线程就不会进入死亡状态.
具体实现过程:
实现代码以下:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"%s", __func__);
[self residentThread];
}
- (void)residentThread {
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(run1) object:nil];
self.thread = thread;
[self.thread start];
}
- (void)run1 {
//这里写须要执行的代码
NSLog(@"run1 -- %@", [NSThread currentThread]);
//一个RunLoop至少须要一个Source或者Timer,在这里添加一个Source1
[[NSRunLoop currentRunLoop]addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop]run];
NSLog(@"未开启RunLoop -- %@", [NSThread currentThread]);
}
复制代码
3.运行后会发现 未开启RunLoop 并不打印,由于RunLoop循环一直没有返回.
为了线程是否还能够继续执行其余任务即没有进入死亡状态,咱们在touchesBegan中调用PerformSelector方法,看看是否会打印.
代码以下:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:nil];
}
- (void)run2 {
NSLog(@"run2 -- %@", [NSThread currentThread]);
}
复制代码
运行代码后点击屏幕,发现能够打印,即线程可以继续执行任务.这样常驻线程就完成了.
1 只有子线程的RunLoop设置退出时间才有用,主线程的RunLoop是没法退出的.即下面这句代码是不会起到使RunLoop退出的做用.
[[NSRunLoop mainRunLoop]runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]];
复制代码
2 RunLoop何时建立和销毁自动释放池
首先咱们要知道RunLoop为何要建立自动释放池?
由于在一个RunLoop运行循环过程当中会产生大量变量和对象,并且大多数变量是不会再使用的.那么若不清理掉这些不用的变量,内存就可能会被堆满.因此RunLoop会按期建立一个自动释放池,而且在特意时间释放掉释放池,并从新再建立一个.
第一次建立: 启动RunLoop的时候 最后一次销毁: 退出RunLoop以前 其余时候的建立和销毁: 在RunLoop进入休眠状态前会释放掉旧的释放池,释放池中的变量也一块儿被销毁了.而后建立出一个新的释放池,用来存放新产生的不用的变量.