iOS多线程-RunLoop

823 阅读8分钟

前言

基本作用

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

main函数中的RunLoop

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

  • 默认启动的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转为CFRunLoopRef

子线程的RunLoop直接通过[NSThread currentThread]方法创建得到,其实是懒加载的

RunLoop相关类

Runloop的五个类

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

这五个类的关系:

  • 在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的处理逻辑

  2. 自动释放池什么时候释放

  • 第一次创建:runloop启动
  • 最后一次销毁:runloop退出
  • 其他时候的创建和销毁:当runloop即将休眠的时候销毁之前的释放池,被唤醒时就重新创建一个新的
  1. observer可以用来做什么?
  • 监听runloop状态
  1. 在开发中如何使用runloop?什么应用场景?
  2. 开启一个常驻线程(让一个子线程不进入消亡状态,等待其他线程发来消息,处理其他事件) 例如一个网络请求,因为网络请求是异步的,且比较耗时,所以我们可以创建一个子线程来负责网络请求的功能
  • 在子线程中开启一个定时器
  • 在子线程中进行一些长期监控