- 原文链接:
- runloop
RunLoop简介
RunLoop 实际上是一个对象,用来处理程序运行过程中出现的各种事件(比如:触摸、UI刷新、定时器、selector等),从而保持程序的持续运行;而且在没有事件处理时,会进入睡眠模式,节省资源,提高性能。
1.RunLoop和线程
线程的作用使用来执行一个或多个任务,线程执行完之后就会退出,不能再执行任务。而RunLoop能够让线程处理任务。
- 每个线程都有唯一一个与之对应的RunLoop对象。
- 我们只能操作当前线程的RunLoop,而不能操作其他线程的RunLoop。
- RunLoop对象再第一次获取RunLoop时创建,销毁则是在线程结束的时候。
- 主线程的RunLoop对象系统会自动创建,而子线程的RunLoop对象需要我们主动创建。
2.RunLoop相关类
RunLoop 有5个类,只有弄懂这几个类的含义,才能深入了解RunLoop运行机制。
- CFRunLoopRef : 代表RunLoop对象;
- CFRunLoopModeRef : RunLoop的运行模式;
- CFRunLoopSourceRef : RunLoop输入源/事件源;
- CFRunLoopTimerRef : RunLoop定时源;
- CFRunLoopObserverRef : 观察者,能够监听RunLoop的状态改变;
一个RunLoop对象(CFRunLoopRef)中包含若干个运行模式(CFRunLoopModeRef)。而一个运行模式又包含若干个输入源(CFRunLoopSourceRef)、定时源(�CFRunLoopTimerRef)、观察者(CFRunLoopObserveRef)。
2.1CFRunLoopRef
CFRunLoopRef是Core Foundation框架下RunLoop对象类,我们可以通过以下方式来获取。
- Core Foundation
- CFRunLoopGetCurrent();//获取当前线程的RunLoop对象。
- CFRunLoopGetMain();//获取主线程的RunLoop对象。
Foundation框架下获取RunLoop对象类的方法:
- Foundation
- [NSRunLoop currentRunLoop];//获取当前线程的RunLoop对象;
- [NSRunLoop mainRunLoop];//获取主线程的RunLoop对象;
2.2CFRunLoopModeRef
系统默认定义了以下几种运行模式(CFRunLoopModeRef):
- kCFRunLoopDefaultMode : App默认运行模式,通常主线程实在这个运行模式下运行。
- **UITrackingRunLoopMode ** :跟踪用户的交互时间(用于追踪Scrollview触摸滑动,保证界面滑动时不受其他Mode影响);
- kCFRunLoopCommonModes : 伪模式,不是一种真正的运行模式,后续有介绍;
- UIInitializationRunLoopMode : App启动时进入的第一个Mode,启动完成后就不再使用;
- GSEventReceiveRunLoopMode : 接受系统内部时间,通常不会用到;
注 : 前三种模式是我们开发中需要用到的模式。
2.3CFRunLoopTimerRef
CFRunLoopRef是定时源,可以理解为基于时间的触发器,基本上可以说是NSTimer;
示例项目:
- 新建一个IOS项目,在Main.storyboard中拖入一个TextView;
- 在ViewController.m文件中写入一下代码;
- (void)viewDidLoad{
[super viewDidLoad];
//定义一个定时器,每两秒调用run方法
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:Yes];
//将定时器添加到当前RunLoop的NSDefaultRunLoopMode下
[ [NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefauRunLoopMode];
}
- (void)run{
NSLog(@"--run");
}
3.然后运行,当拖动Text View时,会发现run方法不打印了,也就是说NSTimer不工作了。而当鼠标松开时,NSTimer又开始正常工作了。
原因:
- 当我们不做任何操作的时候,RunLoop处于NSDefaultRunLoopMode下。
- 当我们拖动Text View的时候,RunLoop就切换到了UITrackingRunLoop模式下工作了。而在这个项目中,NSTimer没有添加到UITrackingRunLoop模式下。所以拖动Text View的时候NSTimer停止了。
- 当我们松开鼠标的时候,RunLoop又切换到了NSDefaultRunLoopMode模式下工作了,所以NSTimer又开始工作了。
解决方法:
- 把NSTimer添加到伪模式(kCFRunLoopCommonModes)下工作;(伪模式(kCFRunLoopCommonModes):这实际上是一种标记模式,意思就是可以在打上Common Modes标记的模式下运行;)因为NSDefaultRunLoopMode和UITrackingRunLoopMode两种模式已经被打上了Common Modes 标记了。
- 具体做法:将添加语句改为:
[ [NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes ];
扩展:
- NSTimer中的schduledTimerWithTimeInterval方法和RunLoop的关系,添加这句代码:
`[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
`
- 这句代码调用了scheduledTimer返回的定时器,NSTimer会自动被加入到RunLoop的NSDefaultRunLoopMode模式下。
2.4CFRunLoopSourceRef
CFRunLoopSourceRef是事件源,CFRunLoopSourceRef有两种分类方法:
- 第一种按照官方文档来分类(就像RunLoop模型图中那样):
- Port-Based Source (基于端口)
- Custom Input Source (自定义)
- Cocoa Perform Selector Source
- 第二种按照函数调用栈来分类:
- Source0:非基于Port;
- Source1:基于Port,通过内核和其他线程通信,接收、分发系统事件;
注:
这两种分类其实没有区别,只不过第一种是通过官方理论来分类,第二种是在实际应用中通过调用函数来分类。
示例项目:
-
在Main.storyboard中添加一个Button按钮,并添加点击动作。
-
然后在点击方法的代码中加入一句输出语句,并打上断点,如下图所示
Paste_Image.png -
然后运行程序,点击按钮;
-
然后在项目中单机下图红色部分:
Paste_Image.png -
可以看到如下图所示就是点击事件产生的函数调用栈:
Paste_Image.png
示例解析:
- 首先程序启动,调用16行的main函数,main函数调用15行UIApplicationMain函数,然后一直往上调用函数,最终调用到0行的BtnClick函数,即点击函数。
- 同时,我们可以看到11行中有Source0,也就是说我们点击事件是属于Source0函数的,点击事件就是在Sources0中处理的。
- 而至于Source1,则是用来接收、分发系统事件,然后分发到Source0中处理的。
2.5CFRunLoopObserverRef
- CFRunLoopObserver是观察者,用来监听RunLoop的状态改变。
CFRunLoopObserverRef可以监听的状态改变有以下几种:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), //即将进入Loop:1
kCFRunLoopBeforeTimers = (1UL << 1), //即将处理Timer:2
kCFRunLoopBeforeSources = (1UL << 2), //即将处理Source:4
kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠:32
kCFRunLoopAfterWaiting = (1UL << 6), //即将从Loop中退出:64
kCFRunLoopExit = (1UL << 7), //即将从Loop中退出:128
kCFRunLoopAllActivities = 0x0FFFFFFFU //监听全部状态改变
};
代码演示:
- 在ViewController.m中添加如下代码:
- (void)viewDidLoad {
[super viewDidLoad];
//创建观察者
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"监听到RunLoop状态发生改变-----%zd",activity);
});
//添加观察者到当前RunLoop中
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
//释放observer;最后添加完需要释放掉
CFRelease(observer);
}
2.运行,打印结果如下图:
Paste_Image.png解析:
- 可以看到,RunLoop状态在不断改变,最终变成了32,也就是即将进入睡眠状态,说明RunLoop之后就会进入睡眠状态。
3.RunLoop原理
- RunLoop运行原理如下图(来源:[](http://blog.ibireme.com/2015/05/18/runloop/)):
官方文档说明的RunLoop逻辑:
- 在每次运行RunLoop的时候,所在线程的RunLoop会自动处理之前为处理的事件,并且通知相关的观察者。
- 具体顺序如下:
- 通知观察者RunLoop已经启动;
- 通知观察者即将要开始的定时器;
- 通知观察者任何即将启动的非基于端口的源;
- 启动任何准备好的非基于端口的源;
- 如果基于端口的源准备好并处于等待状态,立即启动;并进入步骤9;
- 通知观察者线程进入休眠状态;
- 将线程置于休眠,知道下面任一事件发生:
- 某一事件到达基于端口的源;
- 定时器启动;
- RunLoop设置的时间已经超过;
- RunLoop被显示唤醒;
- 通知观察者线程被唤醒;
- 处理未处理的事件
- 如果用户定义的定时器启动,处理定时器时间并重启RunLoop,进入步骤2;
- 如果输入源启动,传递相应的消息
- 如果RunLoop被显示唤醒而且时间还没超过,重启RunLoop,进入步骤2;
- 通知观察者RunLoop结束。
4.RunLoop 实战应用
4.1 NSTimer的使用
- NSTimer的使用在讲解CFRunLoopTimerRef类的时候详细讲解过,具体参考2.3CFRunLoopTimerRef。
4.2 ImageView推迟显示
- 当界面中含有UITableview,而且每个UITableViewCell都有图片时,当我们滚动UITableView的时候,如果有一堆图片需要显示,那么可能会出现卡顿的现象。这时候就可以通过推迟ImageView的显示来解决。
具体有以下两种解决方法:
方法1: 监听UIScrollview的滚动:
- 因为UITableview继承自UIScrollview,所以我们可以通过监听UIScrollview的滚动,实现UIScrollview相关的delegate即可。
方法2: 利用PerformSelector设置当前线程的RunLoop运行模式:
- 利用
performSelector
方法为UIImageView调用setImage:
方法,并利用inModes
将其设置为RunLoop下NSDefaultRunLoopMode运行模式。代码如下:
4.3 后台常驻线程
- 在开发应用程序的过程中,如果后台操作特别频繁,经常会在子线程做一些耗时操作(如:下载文件、后台播放音乐等),最好的解决方法就是能让这条线程永远常驻内存。
- 实现思路:添加一条用于常驻内存的强引用子线程,在该线程的RunLoop下添加一个Source,开启RunLoop;
实现过程如下:
- 在ViewController.m 添加一条抢引用的thread线程属性,代码如下:
@interface ViewController ()
@property (strong, nonatomic) NSThread *thread;
@end
2.在viewDidLoad中创建线程self.thread,使线程启动并执行run1方法,代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
//创建线程,并调用run1方法执行任务
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run1) object:nil];
//开启线程
[self.thread start];
}
/**
*
*
*/
- (void)run1
{
//在这里写要执行的任务
NSLog(@"--------------run1--------------");
//添加这两句代码,开启RunLoop,之后self.thread就变成了常驻线程,可随时添加任务,并交RunLoop处理
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
//测试是否开启了RunLoop,如果开启了RunLoop,则来不了这里,因为RunLoop开启了循环。
NSLog(@"未开启RunLoop");
}
3.运行结果如图,说明这时已经开启了一条常驻线程。
Paste_Image.png
4.接下来我们在touchesBegan中调用PerformSelector,实现点击屏幕的时候往常驻线程添加run2任务。具体代码如下:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//利用performselector,在self.thread的线程中调用run2方法
[self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void)run2
{
NSLog(@"----------run2---------");
}
5.运行,点击屏幕之后打印结果如下,这样我们就实现了常驻内存的需求了。
Paste_Image.png
网友评论