美文网首页iOS Developer
RunLoop入门到崩溃

RunLoop入门到崩溃

作者: 饭饭男 | 来源:发表于2016-11-14 16:11 被阅读151次

先来看看runloop的定义:

一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,通常的代码逻辑是这样的:

function loop() { 
initialize(); 
do { 
  var message = get_next_message();
   process_message(message);
 } while (message != quit);
}
这种模型通常被称作 [Event Loop](http://en.wikipedia.org/wiki/Event_loop)。 Event Loop 在很多系统和框架里都有实现,比如 Node.js 的事件处理,比如 Windows 程序的消息循环,再比如 OSX/iOS 里的 RunLoop。实现这种模型的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。
所以,RunLoop 实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 "接受消息->等待->处理" 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。

OSX/iOS 系统中,提供了两个这样的对象:NSRunLoop 和 CFRunLoopRef。CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。

        runloop和线程的关系            

线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。

创建:主线程Runloop已经创建好了,子线程的runloop需要手动创建 创建方式:

NSRunLoop *currentRunloop = [NSRunLoop currentRunLoop];
子线程创建的runloop还需要手动的开启:
[currentRunloop run];
生命周期:Runloop在第一次获取时创建,在线程结束时销毁
    获取RunLoop对象

Foundation

[NSRunLoop currentRunLoop];获取当前线程的 RunLoop 对象
[NSRunLoop mainRunLoop];获取主线程 RunLoop 对象

Core Foundation

CFRunLoopGetCurrent();获取当前线程的 RunLoop 对象
CFRunLoopGetMain();获取主线程的RunLoop对象

    RunLoop 相关类

Core Foundation 中相关 RunLoop 的 5 个类
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef

CFRunLoopModeRef 代表 RunLoop 的运行模式
一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。

959078-61f4b3e698741289.png

每次 RunLoop 启动时,只能指定其中一个 Mode,这个 Mode 被称作 CurrentMode。
如果需要切换 Mode,只能退出 RunLoop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响

系统默认注册了5个 Mode:
KCFRunLoopDefaultMode
,App 的默认 Mode,通常主线程在这个 Mode 下运行。
UITrackingRunLoopMode
,界面跟着 Mode,用于 ScrollView 追踪触摸滑动,保证节目滑动时不受其他 Mode 影响。
UIInitializationRunLoopMode
,在刚启动 App 时进入的第一个 Mode,启动完成后就不再使用。
GSEventReceiveRunLoopMode
,接受系统事件的内部 Mode,通常用不到。
KCFRunLoopCommonModes
,这是一个占位用的 Mode,不是一种真正的 Mode。

      runloop的实际使用

RunLoop运行模式对NSTimer定时器的影响
首先创建一个定时器

          NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(task) userInfo:nil repeats:YES];

我们创建一个tableview 把定时器对象添加到runloop中,并且制定runloop的运行模式为默认:只有当runloop的运行模式为NSDefaultRunLoopMode的时候定时器才工作,也就是说这时候如果滑动Ttableview,定时器就不工作了

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
如果想在滚动tableview的时候,定时器也工作,可以:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

但是如果这样做的话,当我们停止滚动的时候定时器又不工作了
4.有时候我们需要在默认情况下以及在滚动的时候都让定时器工作,这时候我们就可以:

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

由此可见:NSRunLoopCommonModes = NSDefaultRunLoopMode & UITrackingRunLoopMode
拓展:①scheduledTimerWithTimeInterval方法:创建定时器并默认添加到当前线程的Runloop中指定默认运行模式
②timerWithTimeInterval:创建定时器,如果该定时器要工作还需要添加到runloop中并指定相应的运行模式

通过以上代码我们不难看出,NSTimer的定时器是受运行模式影响的,,而开发中我们有时候彻底去除这种影响,很显然,NSTimer定时器不能做到这点,这时,我们可以使用GCD的定时器。

有一种场景就是在子线程去开启一个 NSTimer,你说这个 timer 会不会执行?我说这个 timer 不会执行,因为 timer 的执行是依靠与 RunLoop 的,子线程都没有开启 RunLoop,所以也就不会执行。解决这个问题只需要开启 RunLoop 即可

     imageView 的显示
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"1KR2C08-9"] afterDelay:2.0f];
这个同上面的NSTimer
类似,Mode 默认为NSDefaultRunLoopMode
, 在滑动一个 ScrollView 控件的时候,那么这个 image 对象就不会显示在 imageView 控件上。要想在滑动 ScrollView 控件的时候,也能让 image 对象显示在 imageView 控件上,使用另外一个带有 Modes 的 performSelector 即可,如下:
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"1KR2C08-9"] afterDelay:2.0 inModes:@[NSRunLoopCommonModes]];
GCD定时器GCD定时器是不受运行模式的影响的,因此,以后尽量使用此定时器,,该定时器的具体参数如下所示:
-(void)touchesBegan:(NSSet<UITouch *> *)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实现常驻线程
众所周知,我们手动开启的子线程在执行完任务之后就会销毁,而有时候我们需要一个子线程在执行完当前任务后,不要销毁,等我们需要的时候再来执行其它任务,这就用到了常驻线程。
假设我们又这样一个需求,但我们点击按钮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的自动释放池什么时候创建释放?

  1. 当runloop进入的时候会创建一个自动释放池。
    2)当runloop退出的时候会把之前的自动释放池销毁。
    3)当runloop即将进入休眠的时候会把之前的自动释放池先销毁,然后创建一个新的自动释放池。
    参考:http://www.jianshu.com/p/884299a430c7
    http://blog.ibireme.com/2015/05/18/runloop/
    http://www.jianshu.com/p/a9ec73251fe6

相关文章

  • RunLoop入门到崩溃

    先来看看runloop的定义: 一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机...

  • RunLoop汇总

    RunLoop 入门 看我就够了RunLoop 已入门?不来应用一下?ibireme的文章,关于RunLoop背后...

  • 底层原理探究(二)RunLoop

    转自: 老司机出品——源码解析之RunLoop详解入门使用: RunLoop入门 看我就够了孙源的Runloop视...

  • RunLoop从入门到进阶

    一个线程一次只能执行一个任务,执行完毕后就会退出。如果需要一个机制,让线程能随时处理事件,处理完毕后并不退出,代码...

  • 10.3 runloop 的实际应用

    runloop实际中的应用 控制线程生命周期(线程保活)、崩溃的起死回生 runloop和performselec...

  • iOS复习之RunLoop

    1、事件循环2、用户态3、核心态4、常驻线程 主要 RunLoop文章:RunLoop入门 看我就够了iOS - ...

  • 区块链-入门到崩溃

    原文链接:http://mochain.cc/portal.php?mod=view&aid=12 我会把区块链学...

  • RunLoop入门

    什么是RunLoop Run Loop是一让线程能随时处理事件但不退出的机制。RunLoop 实际上是一个对象,这...

  • RunLoop入门

    我们的项目其实就是一个RunLoop,比如main函数的UIApplicationMain就是一个死循环函数,它在...

  • RunLoop入门

    之前在使用NSTimer时,遇到一个问题,NSTimer启动了,但是只会出现一次定时,之后就不起作用。问了同...

网友评论

    本文标题:RunLoop入门到崩溃

    本文链接:https://www.haomeiwen.com/subject/eomhpttx.html