iOS多线程-RunLoop

前言

基本做用面试

  • 保持程序的持续运行
  • 处理App中的各类事件(好比触摸事件、定时器事件、Selector事件)
  • 节省CPU资源,提升程序性能(该作事时作事,该休息时休息)

main函数中的RunLoop

最下面的UIApplicationMain函数内部就启动了一个RunLoop,因此UIApplicationMain就一直没有返回,保持了程序的持续运行markdown

  • 默认启动的RunLoop是跟主线程相关的

RunLoop

iOS有两套API来访问和使用RunLoop网络

  1. Foundation
  • NSRunLoop
  1. Core Foundation
  • CFRunLoopRef

NSRunLoop和CFRunLoopRef都表明着RunLoop对象 可是NSRunLoop是基于CFRunLoopRef的一层OC包装 ,因此更底层的是CFRunLoopRef异步

RunLoop与线程

  • 每条线程都有惟一的一个与之对应的RunLoop对象
  • 主线程的RunLoop以及自动建立好了,子线程的RunLoop须要主动建立
  • RunLoop在第一次获取时建立,在线程结束时销毁

得到RunLoop对象

是如何保证每条线程有惟一一个对应的RunLoop对象的呢?函数

  • 系统会先判断是否有RunLoop存在,若是不存在,就会建立一个RunLoop,并建立一个字典,里面存放了线程-线程对应的RunLoop

注意,经过NSRunLoop和CFRunLoopRef获得的RunLoop也仍是不一样的对象,可是能够经过.getCFRunLoop将NSRunLoop转为CFRunLoopRefoop

子线程的RunLoop直接经过[NSThread currentThread]方法建立获得,实际上是懒加载的性能

RunLoop相关类

Runloop的五个类atom

  1. CFRunLoopRef:表明 RunLoop 的对象
  2. CFRunLoopModeRef:表明 RunLoop 的运行模式
  3. CFRunLoopSourceRef:就是 RunLoop 模型图中提到的输入源 / 事件源
  4. CFRunLoopTimerRef:就是 RunLoop 模型图中提到的定时源
  5. CFRunLoopObserverRef:观察者,可以监听 RunLoop 的状态改变

这五个类的关系:spa

  • 在runloop(CFRunLoopRef)中有多个运行模式(CFRunLoopModeRef),可是runloop只能选择一种运行模式,这个mode就叫作CurrentMode(就比如空调有制冷制热等多种模式,可是每次开启空调只能选择一种模式)
  • 若是须要切换mode,只能退出runloop,再从新指定一个mode进入
    • 这样作主要是为分隔开不一样组的source/timer/observer(CFRunLoopSourceRef/CFRunLoopTimerRef/CFRunLoopObserverRef),让其不相互影响

  • 每一个mode里面至少要有一个timer或者是source,只有一个observer是不行的
  • 每一个mode能够包含若干个ource/timer/observer

CFRunLoopModeRef

系统默认注册了五个mode 线程

应用场景一

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self timer];
}

-(void)timer{
    //1. 建立计时器
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    //2. 将计时器添加到runloop中
    //第一个参数是计时器,第二个参数是runloop的mode,这里选择默认mode
    [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
    
}

-(void)run{
    NSLog(@"run---%@----%@",[NSThread currentThread],[NSRunLoop currentRunLoop].currentMode);
}
复制代码

当你点击模拟器时,打印结果以下,能够看到每隔2s进行一次打印

2021-03-10 10:37:05.571371+0800 runloop1[1771:51979] run---<NSThread: 0x600001f44a00>{number = 1, name = main}----kCFRunLoopDefaultMode
2021-03-10 10:37:07.572174+0800 runloop1[1771:51979] run---<NSThread: 0x600001f44a00>{number = 1, name = main}----kCFRunLoopDefaultMode
2021-03-10 10:37:09.571467+0800 runloop1[1771:51979] run---<NSThread: 0x600001f44a00>{number = 1, name = main}----kCFRunLoopDefaultMode
复制代码

可是当你向storyboard中添加一个textView会发生什么状况呢?

咱们来看看场景二

应用场景二

你会发现当你点击背景时,正常每隔2s进行打印,可是当你滑动textView时,打印中止,且你再中止滑动,打印又从新开始

这是为何?

  • 由于在你拖动textView后,runloop会自动进入到页面追踪模式,当进入页面追踪模式后,就不会再理会计时器了
  • 当你中止拖动后,runloop又自动进入到默认模式,timer继续运行,因此会继续打印

解决方法,修改runloop的mode类型,改成UITrackingRunLoopMode(界面追踪模式)

-(void)timer{
    //1. 建立计时器
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    //2. 将计时器添加到runloop中
    [[NSRunLoop currentRunLoop]addTimer:timer forMode:UITrackingRunLoopMode];
}
复制代码

从新运行,会发现当你改成滑动textView时,打印结果以下

2021-03-10 10:55:22.039501+0800 runloop1[2084:70476] run---<NSThread: 0x600002eb4880>{number = 1, name = main}----UITrackingRunLoopMode
2021-03-10 10:55:22.367009+0800 runloop1[2084:70476] run---<NSThread: 0x600002eb4880>{number = 1, name = main}----UITrackingRunLoopMode
复制代码

能够看到此时的模式是界面追踪模式

那么如何既点击view时启动计时器打印,拖动textview时也打印呢?

接下来进入下一场景

应用场景三

但愿达到 既点击view时启动计时器打印,拖动textview时也打印 有两种方式:

  1. 添加两种方式
-(void)timer{
    //1. 建立计时器
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    //2. 将计时器添加到runloop中
    [[NSRunLoop currentRunLoop]addTimer:timer forMode:UITrackingRunLoopMode];
    [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
}
复制代码

打印结果以下

2021-03-10 11:01:20.588388+0800 runloop1[2269:79479] run---<NSThread: 0x60000038c240>{number = 1, name = main}----kCFRunLoopDefaultMode
2021-03-10 11:01:22.588833+0800 runloop1[2269:79479] run---<NSThread: 0x60000038c240>{number = 1, name = main}----kCFRunLoopDefaultMode
2021-03-10 11:01:24.588094+0800 runloop1[2269:79479] run---<NSThread: 0x60000038c240>{number = 1, name = main}----UITrackingRunLoopMode
2021-03-10 11:01:26.588974+0800 runloop1[2269:79479] run---<NSThread: 0x60000038c240>{number = 1, name = main}----UITrackingRunLoopMode
复制代码
  1. 使用占位用mode:NSRunLoopCommonModes

NSRunLoopCommonModes = kCFRunLoopDefaultMode + UITrackingRunLoopMode

  • 占用其实就是一种标签,凡是添加到NSRunLoopCommonModes中的事件都会被同时添加到大赏common标签的运行模式上
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
复制代码

定时器的建立有两种方式,上面的场景中使用的是第一种,也就是timerWithTimeInterval的方式,接下来的场景咱们使用第二种方式scheduledTimerWithTimeInterval

应用场景四:

回忆一下咱们刚刚上面建立定时器的方法还须要本身将定时器添加到runloop当中,可是scheduledTimerWithTimeInterval建立的定时器是不须要这样的,系统会帮你作,而且设置运行模式位默认mode

-(void)timer2{
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
}
复制代码

还有一个问题,若是咱们的timer是在子线程中建立的,会出现什么问题?

  • 很显然,不会有任何反应

这是为何呢?缘由很简单

  • 由于你的timer是在子线程中运行的,可是你的子线程并无与之对应的runloop(由于你没有建立)
  • 因此只要咱们再手动建立子线程对应的runloop便可
  • 主线程的runloop默认建立,子线程的runloop须要手动建立
-(void)timer2{
    //1. 建立子线程的runloop
    NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
    //2. 建立计时器
    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    //3. 启动runloop
    [currentRunLoop run];
}
复制代码

RunLoop应用

  • NSTimer
  • 常驻线程
  • ImageView显示
  • 自动释放池
  • performSelector

重点来看这个常驻线程

  • 通常的子线程,在线程里的任务都执行完毕后就会进入死亡状态
  • 这时候即使start也没法从新开启
  • 为了让咱们的线程知足咱们须要使用时就使用,不须要使用时就处于等待状态,随时能够从新使用呢?

这时候就须要咱们的常驻线程了

注意,咱们想要达到的目的是让一个线程不在他的任务执行完毕后就死亡,而是进入等待模式,在须要时再从新使用该线程

解决方法:

  • 开启线程的runloop
  • 咱们知道runloop是能够知足该作事时作事,该休息时休息的要求

接下来进入具体应用状况

定义三个按钮以下

  • 首先,咱们定义一个线程属性
@property(nonatomic,strong) NSThread *thread;
复制代码
  • 点击建立线程按钮时建立线程
    • 注意咱们在建立线程的同时调用了createRunLoop方法来建立runloop
- (IBAction)createClickBtn:(id)sender {
    // 建立线程
    self.thread = [[NSThread alloc]initWithTarget:self selector:@selector(createRunLoop) object:nil];
    [self.thread start]; 
}
复制代码

建立runloop有两种设置方式

  1. 一种是设置timer
- (void)createRunLoop{
    //1. 得到子线程对应的runloop
    NSRunLoop *currentLoop = [NSRunLoop currentRunLoop];
    //2. 在runloop中设置一个timer
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    //3. 将timer添加到runloop中
    [currentLoop addTimer:timer forMode:NSDefaultRunLoopMode];
    //4. 启动runloop
    [currentLoop run];
}
复制代码
  1. 一种是设置source(这个方式更可取,由于咱们没有必要设置一个计时器)
- (void)createRunLoop{
    //1. 得到子线程对应的runloop
    NSRunLoop *currentLoop = [NSRunLoop currentRunLoop];
    //2. 在runloop中设置一个source
    [currentLoop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    //3. 启动runloop
    [currentLoop run];
}
复制代码
  • 定义任务
- (IBAction)task1ClickBtn:(id)sender {
    [self performSelector:@selector(task1) onThread:self.thread withObject:nil waitUntilDone:YES];
}


- (IBAction)task2ClickBtn:(id)sender {
    [self performSelector:@selector(task2) onThread:self.thread withObject:nil waitUntilDone:YES];
}

-(void)task1{
    NSLog(@"task1---%@",[NSThread currentThread]);
}


-(void)task2{
    NSLog(@"task2---%@",[NSThread currentThread]);
}

//这个run函数是用于计时器状况时的
-(void)run{
    NSLog(@"%s",__func__);
}
复制代码

运行结果以下

能够看到当你交换点击两个按钮时,任务也是交替执行的,而且是在同一线程下

RunLoop面试题

  1. 什么是runloop?

  1. runloop的处理逻辑

  1. 自动释放池何时释放
  • 第一次建立:runloop启动
  • 最后一次销毁:runloop退出
  • 其余时候的建立和销毁:当runloop即将休眠的时候销毁以前的释放池,被唤醒时就从新建立一个新的
  1. observer能够用来作什么?
  • 监听runloop状态
  1. 在开发中如何使用runloop?什么应用场景?
  2. 开启一个常驻线程(让一个子线程不进入消亡状态,等待其余线程发来消息,处理其余事件)

例如一个网络请求,由于网络请求是异步的,且比较耗时,因此咱们能够建立一个子线程来负责网络请求的功能

  • 在子线程中开启一个定时器
  • 在子线程中进行一些长期监控

相关文章
相关标签/搜索