RunLoop
1.RunLoop的基本作用
- 1.保持程序的持续运行
- 2.处理app中的各种事件(比如触摸事件、定时器事件、selector事件
- 3.节省CPU资源,提高程序性能,有事情就做事情,没事情就休息
2.RunLoop与线程
- 1.关系:一个Runloop对应着一条唯一的线程
- 2.创建:主线程Runloop已经创建好了,子线程的runloop需要手动创建 创建方式:
NSRunLoop *currentRunloop = [NSRunLoop currentRunLoop];
子线程创建的runloop还需要手动的开启:
[currentRunloop run];
- 3.生命周期:Runloop在第一次获取时创建,在线程结束时销毁
2.RunLoop运行模式对NSTimer定时器的影响
- 1.首先创建一个定时器
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(task) userInfo:nil repeats:YES];
这里的task为一个普通的打印操作,这时候往控制器的view里拖一个Text View,
- 2.把定时器对象添加到runloop中,并且制定runloop的运行模式为默认:只有当runloop的运行模式为NSDefaultRunLoopMode的时候定时器才工作,也就是说这时候如果滑动Text View,定时器就不工作了
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
- 3.如果想在滚动Text View的时候,定时器也工作,可以:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
但是如果这样做的话,当我们停止滚动的时候定时器又不工作了
- 4.有时候我们需要在默认情况下以及在滚动的时候都让定时器工作,这时候我们就可以:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
由此可见:NSRunLoopCommonModes = NSDefaultRunLoopMode & UITrackingRunLoopMode
拓展:①scheduledTimerWithTimeInterval方法:创建定时器并默认添加到当前线程的Runloop中指定默认运行模式
②timerWithTimeInterval:创建定时器,如果该定时器要工作还需要添加到runloop中并指定相应的运行模式
通过以上代码我们不难看出,NSTimer的定时器是受运行模式影响的,,而开发中我们有时候彻底去除这种影响,很显然,NSTimer定时器不能做到这点,这时,我们可以使用GCD的定时器。
GCD定时器
GCD定时器是不受运行模式的影响的,因此,以后尽量使用此定时器,,该定时器的具体参数如下所示:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//01 创建定时器对象
/*
第一个参数:是一个宏,表示要创建的是一个定时器
第二个参数和第三个参数:默认总是传0 描述信息
第四个参数:队列 决定代码块(dispatch_source_set_event_handler)在哪个线程中调用(主队列-主线程)
*/
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
//02 设置定时器
/*
第一个参数:定时器对象
第二个参数:定时器开始计时的时间(开始时间)
第三个参数:设置间隔时间 GCD的时间单位:纳秒
第四个参数:精准度
*/
//这句代码是设置开始两秒之后再调用
dispatch_time_t t = dispatch_time(DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC);
dispatch_source_set_timer(timer, t, 2.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
//03 GCD定时器时间到了之后要执行的任务
dispatch_source_set_event_handler(timer, ^{
NSLog(@"GCD----%@",[NSThread currentThread]);
});
//04 默认是在挂起状态,需要手动恢复执行
dispatch_resume(timer);
//如果没有一个强指针指向,一创建就回被释放。
self.timer = timer;
}
CFRunLoopModeRef
- a.基本说明:
- CFRunloopModeRef代表着Runloop的运行模式
- 一个Runloop中可以有多个mode,一个mode里面又可以有多个source\observer\timer等等
- 每次runloop启动的时候,只能指定一个mode,这个mode被称为该Runloop的当前mode
- 如果需要切换mode,只能先退出当前Runloop,再重新指定一个mode进入
- 这样做主要是为了分割不同组的定时器等,让他们相互之间不受影响
- b.Model的分类:
- kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
- UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动
- UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
- GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
- kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode
CFRunloopSourceRef
分类(根据函数调用栈来区分):
- 1.Source0:非基于Port的 :凡是用户主动触发事件都是Source0事件
- 2.Source1:基于Port的:凡是系统事件都是Source1事件
CFRunLoopObserverRef
作用:监听运行循环的状态
selecter事件与RunLoop之间的关系
- 默认情况下,selecter事件是被添加到当前的runloop中执行的,并且指定了运行模式为默认,由此可见,performSelecter事件是受运行模式的影响的,仔细查看以下代码,看看有什么问题:
[NSThread detachNewThreadSelector:@selector(task) toTarget:self withObject:nil];
-(void)task{
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"Snip20160713_9"] afterDelay:3.0];
NSLog(@"+++++");
}
一个显而易见的问题就是我们在子线程中来设置显示图片,然而抛开这个问题不管,图片依旧不会被设置上去,因为在task中缺少一个运行循环,我们需要手动开启一个子运行循环才可以。
继续查看一下代码,图片会被设置到imageView上面吗?
[NSThread detachNewThreadSelector:@selector(task) toTarget:self withObject:nil];
-(void)task{
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
[runloop run];
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"Snip20160713_9"] afterDelay:3.0];
NSLog(@"+++++");
}
答案是否定的,因为我们虽然开启了子运行循环,但是当我们开启这个循环的时候,当前循环里既没有source事件(包括timer事件),也没有selecter事件,于是循环立刻就退出了。正确的书写方式如下:
[NSThread detachNewThreadSelector:@selector(task) toTarget:self withObject:nil];
-(void)task{
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"Snip20160713_9"] afterDelay:3.0];
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
[runloop run];
NSLog(@"+++++");
}
注意:runLoop是一个死循环,因此++++是不会打印的
使用RunLoop实现常驻线程
众所周知,我们手动开启的子线程在执行完任务之后就会销毁,而有时候我们需要一个子线程在执行完当前任务后,不要销毁,等我们需要的时候再来执行其它任务,这就用到了常驻线程。
- 假设我们又这样一个需求,但我们点击按钮1的时候会开启一条子线程来执行run1任务,当我们点击按钮2的时候,再让刚才的线程来执行run2任务,具体实现代码如下:
- (IBAction)btn1:(id)sender {
//01 创建线程,执行任务
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(run1) object:nil];
//02 执行任务
[thread start];
self.thread = thread;
}
为了不让刚开启的线程销毁,我们需要给它添加一个运行循环,保证它不释放:
-(void)run1
{
NSLog(@"run1---%@",[NSThread currentThread]);
//001 获得当前线程对应的runloop对象
NSRunLoop *currentRunloop = [NSRunLoop currentRunLoop];
//002 为runloop添加input soucre或者是timer souce或selecter事件(最好就是一个基于端口的事件,这样就不会去执行不必要的方法)
[currentRunloop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
//以下为其它保证程序运行的方案,不推荐使用。
//[self performSelector:@selector(run3) withObject:nil afterDelay:3.0];
//[NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
//003 启动runloop
//runUntilDate |run 内部都指定了运行模式为默认
[currentRunloop run];
}
按钮2:
- (IBAction)goOnBtnClick:(id)sender {
//让之前创建的线程继续执行
[self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:YES];
}
-(void)run2
{
NSLog(@"run2---%@",[NSThread currentThread]);
}
RunLoop的自动释放池
runloop的自动释放池什么时候创建释放?
-
- 当runloop进入的时候会创建一个自动释放池。
- 2)当runloop退出的时候会把之前的自动释放池销毁。
- 3)当runloop即将进入休眠的时候会把之前的自动释放池先销毁,然后创建一个新的自动释放池。