iOS多线程-RunLoop

1.RunLoop简介

  • 从字面意思讲跑圈,运行循环。RunLoop就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束,函数返回。
    RunLoop
  • 基本作用
    1. 保持程序的持续运行;
    2. 处理App中的各种事件(比如触摸事件、定时器事件、Selector事件);
    3. 节省CPU资源,提高程序性能:有事处理的时候做事,没有事件处理的时候,会进入睡眠模式。
  • 没有RunLoop时模拟如下:
    没有RunLoop
    没有RunLoop的情况下,第3行后程序就结束了。
  • 有RunLoop时模拟如下:
    有RunLoop
    有RunLoop的情况下,由于main函数里面启动了个RunLoop,所以程序并不会马上退出,保持持续运行状态。
  • main函数中的Runloop
    1. UIApplication函数内部就启动了一个Runloop该函数返回一个int类型的值;
    2. 这个默认启动的Runloop是跟主线程相关联的。
  • iOS中有2套API来访问和使用RunLoop:
    1. CoreFoundation里面的CFRunLoopRef
      CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。CFRunLoopRef是开源的,其源码链接如下:
      CFRunLoopRef开源代码下载
    2. Foundation里面的NSRunLoop
      NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。
      RunLoop苹果官方文档
    3. NSRunLoopCFRunLoopRef都代表着RunLoop对象

2.Runloop与线程

  • 一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,这时就用到RunLoop
  • Runloop和线程的关系
    1. 线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。
    2. 线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时(懒加载),RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。

3.RunLoop实现原理

下图是苹果官方文档给出的Runloop运行原理图:
运行循环的结构及其来源
从上图中可以看出,RunLoop就是线程中的一个循环,RunLoop在循环中会不断检测,通过Input sources(输入源)和Timer sources(定时源)两种源等待接受事件;然后对接受到的事件通知线程进行处理,并在没有事件的时候进行休息。

4.RunLoop 对外的接口

CoreFoundation 里面关于 RunLoop 有5个类:
CFRunLoopRef RunLoop的对象
CFRunLoopModeRef RunLoop的运行模式
CFRunLoopSourceRef 输入源/事件源
CFRunLoopTimerRef Timer事件
CFRunLoopObserverRef 监听者,监听RunLoop的状态改变
Runloop和相关类之间的关系图:
关系图

  • 一个RunLoop对象(CFRunLoopRef)中包含若干个运行模式(CFRunLoopModeRef)。而每一个运行模式下又包含若干个输入源(CFRunLoopSourceRef)、Timer事件(CFRunLoopTimerRef)、观察者(CFRunLoopObserverRef)。
  • 每次RunLoop启动时,只能指定其中一个Mode,这个Mode被称作 CurrentMode。如果需要切换Mode,只能退出RunLoop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

4.1.CFRunLoopRef/NSRunLoop(RunLoop对象)

  • CFRunLoopRef就是CoreFoundation框架下RunLoop对象类,不能直接创建,只能获取:
    CFRunLoopGetMain(); // 获得主线程的RunLoop对象
    CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
  • Foundation框架下获取RunLoop对象类的方法如下:
    [NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
    [NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象

4.2.CFRunLoopModeRef(运行模式)

  • CFRunloopModeRef代表着Runloop的运行模式,一个Runloop中可以有多个Mode,一个Mode里面又可以有多个source\observer\timer,每次runloop启动的时候,只能指定一个Mode,这个Mode被称为该Runloop的当前Mode,如果需要切换Mode,只能先退出当前Runloop,再重新指定一个Mode进入,分割不同组的定时器等,让他们相互之间不受影响。
  • 系统默认注册了5个Mode
    1. kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行。
    2. UITrackingRunLoopMode:跟踪用户交互事件Mode(ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。)
    3. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
    4. GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
    5. kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode。

4.3.CFRunLoopSourceRef

CFRunLoopSourceRef是事件源,CFRunLoopSourceRef有两种分类方法:

  • 第一种按照官方文档来分类(就像RunLoop模型图中那样):
    Port-Based Sources(基于端口)
    Custom Input Sources(自定义)
    Cocoa Perform Selector Sources
  • 第二种按照函数调用栈来分类:
    Source0 :非基于Port
    Source1:基于Port,通过内核和其他线程通信,接收、分发系统事件
  • 可以通过打断点的方式查看一个方法的函数调用栈,如下图所示,打断点拦截点击按钮事件
    查看函数调用栈
    结果分析:程序启动,调用17行的main函数,main函数调用16行UIApplicationMain函数,然后一直往上调用函数,最终调用到0行的btnClick函数,即点击函数。同时我们可以看到11,12行中有Sources0,也就是说我们点击事件是属于Sources0函数的,点击事件就是在Sources0中处理的。而至于Sources1,则是用来接收、分发系统事件,然后再分发到Sources0中处理的。

4.4.CFRunLoopTimerRef

  • CFRunLoopTimerRef是定时源,理解为基于时间的触发器,时间到了我就触发一个事件,触发一个操作。基本上说的就是NSTimer。下面结合CFRunLoopModeRef举例说明:
    添加一个UITextView,在viewDidLoad方法中创建定时器并每两面调用一次block中的方法:
- (void)viewDidLoad {
    [super viewDidLoad];
    __block NSInteger num = 0;
    //创建定时器 ,没两秒调用一下block里面的方法
    NSTimer * timer = [NSTimer timerWithTimeInterval:2.0 repeats:YES block:^(NSTimer * `_Nonnull` timer) {
        num ++;
        NSLog(@"%zd次",num);
    }];
    //将定时器添加到当前RunLoop的NSDefaultRunLoopMode下
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
}
  • 结果可见:我们行任何操作的话,定时器会稳定的每隔2秒调用block中的打印方法;但当我们滚动UITextView时,打印就会中断(暂停);停止操作后又会继续打印。
  • 结果分析:当我们不做任何操作的时候,RunLoop处于NSDefaultRunLoopMode下;当我们拖动UITextView的时候,RunLoop就结束NSDefaultRunLoopMode,切换到了UITrackingRunLoopMode模式下,这个模式下没有添加NSTimer,所以我们的NSTimer就不工作了;当我们松开鼠标的时候,RunLoop就结束UITrackingRunLoopMode模式,又切换回NSDefaultRunLoopMode模式,所以NSTimer就又开始正常工作了。
  • RunLoop的模式改为UITrackingRunLoopMode模式,会发现只有在滚动UITextView时,定时器的block中的打印方法才会调用。
  • 要想在两种模式下定时器都能正常工作,就需要用到伪模式(kCFRunLoopCommonModes,NSFoundation框架下为NSRunLoopCommonModes
    伪模式不是一种真实的模式,而是一种标记模式,意思就是可以在打上CommonModes标记的模式下运行,如下修改即可:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
  • NSTimer中的scheduledTimerWithTimeInterval方法和RunLoop的关系如下:
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
//等等同于下面两行代码
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

4.5.CFRunLoopObserverRef

  • CFRunLoopObserverRef是观察者,用来监听RunLoop的状态改变、可以监听的状态改变有以下几种:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),               // 即将进入RunLoop:1
    kCFRunLoopBeforeTimers = (1UL << 1),        // 即将处理Timer:2    
    kCFRunLoopBeforeSources = (1UL << 2),       // 即将处理Source:4
    kCFRunLoopBeforeWaiting = (1UL << 5),       // 即将进入休眠:32
    kCFRunLoopAfterWaiting = (1UL << 6),        // 即将从休眠中唤醒:64
    kCFRunLoopExit = (1UL << 7),                // 即将从RunLoop中退出:128
    kCFRunLoopAllActivities = 0x0FFFFFFFU       // 监听全部状态改变  
};

监听示例如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    //创建监听者
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
         NSLog(@"监听到RunLoop状态发生改变---%zd",activity);
    });
    //添加观察者到当前RunLoop中
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    //最后添加完需要释放掉
    CFRelease(observer);
}

打印结果如下:
CFRunLoopObserverRef
结果可见:RunLoop的状态在不断的改变,最终变成了状态 32,也就是即将进入睡眠状态,说明RunLoop最后就会进入睡眠状态。

5.Runloop运行逻辑

  • 根据苹果在文档里的说明,RunLoop运行逻辑大致如下:
    这里写图片描述
  • 在每次运行开启RunLoop的时候,所在线程的RunLoop会自动处理之前未处理的事件,并且通知相关的观察者。具体的顺序如下:
    1. 通知观察者RunLoop已经启动
    2. 通知观察者即将要开始的定时器
    3. 通知观察者任何即将启动的非基于端口的源
    4. 启动任何准备好的非基于端口的源
    5. 如果基于端口的源准备好并处于等待状态,立即启动;并进入步骤9
    6. 通知观察者线程进入休眠状态
    7. 将线程置于休眠知道任一下面的事件发生:
      (1)某一事件到达基于端口的源
      (2)定时器启动
      (3)RunLoop设置的时间已经超时
      (4)RunLoop被显示唤醒
    8. 通知观察者线程将被唤醒
    9. 处理未处理的事件
      (1)如果用户定义的定时器启动,处理定时器事件并重启RunLoop。进入步骤2
      (2)如果输入源启动,传递相应的消息
      (3)如果RunLoop被显示唤醒而且时间还没超时,重启RunLoop。进入步骤2
    10. 通知观察者RunLoop结束

6.RunLoop应用

  • NSTimer的使用,上面已经讲过。
  • ImageView推迟显示。
    当界面中含有UITableView,而且每个UITableViewCell里边都有图片。这时候当我们滚动UITableView的时候,如果有一堆的图片需要显示,那么可能会出现卡顿的现象。
    怎么解决这个问题呢?
    1. 监听UIScrollView的滚动
      因为UITableView继承自UIScrollView,所以我们可以通过监听UIScrollView的滚动,实现UIScrollView相关delegate即可。
    2. 利用PerformSelector设置当前线程的RunLoop的运行模式
      利用performSelector方法为UIImageView调用setImage:方法,并利用inModes将其设置为RunLoopNSDefaultRunLoopMode运行模式。代码如下:
- (void)viewDidLoad {
    [super viewDidLoad];
    __block NSInteger num = 0;
//    创建定时器 ,没两秒调用一下block里面的方法
    NSTimer * timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        num ++;
        NSLog(@"%zd秒",num);
    }];
    //将定时器添加到当前RunLoop的NSDefaultRunLoopMode下
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    //延迟6秒设置图片
    [self.imageV performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"we"] afterDelay:6.0 inModes:@[NSDefaultRunLoopMode]];
}

结果如下:
图片延迟显示
结果可见:当滚动UITextView时,6秒后图片并没有显示,当停止滚动时图片才显示。

  • 常驻后台线程(很常用)
    我们在开发应用程序的过程中,如果后台操作特别频繁,经常会在子线程做一些耗时操作(下载文件、后台播放音乐等),我们最好能让这条线程永远常驻内存。
    做法:添加一条用于常驻内存的强引用的子线程,在该线程的RunLoop下添加一个Sources,开启RunLoop

示例:

#import "ViewController.h"
@interface ViewController ()
@property(nonatomic ,strong) NSThread * thread;
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    //初始化线程
    self.thread = [[NSThread alloc]initWithBlock:^{
        NSLog(@"runThread:%@",[NSThread currentThread]);
        // 添加下边两句代码,就可以开启RunLoop,之后self.thread就变成了常驻线程,可随时添加任务,并交于RunLoop处理
        NSRunLoop * currentRL = [NSRunLoop currentRunLoop];
        [currentRL addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
        [currentRL run];
        // 如果开启RunLoop,则来不了这里,因为RunLoop开启了循环。
        NSLog(@"NOopenRunLoop");
    }];
    //开启线程
    [self.thread start];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    // 利用performSelector,在self.thread的线程中调用run2方法执行任务
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:YES modes:@[NSDefaultRunLoopMode]];
}
- (void)test{
    NSLog(@"testThread:%@",[NSThread currentThread]);
}

长驻后台线程
结果可见:除了之前打印的runThread外,每当我们点击屏幕,都打印testThread,但是没有打印NOopenRunLoop说明开启了RunLoop循环。


参考文档:
深入理解RunLoop
RunLoop苹果官方文档


iOS多线程相关文章:
iOS多线程简述
iOS多线程-pthread、NSThread
iOS多线程-GCD
iOS多线程-NSOperation, NSOperationQueue
iOS多线程-RunLoop
OC单例模式详解

猜你喜欢

转载自blog.csdn.net/bolted_snail/article/details/79803250