何为RunLoop?RunLoop有哪些应用场景?

一、RunLoop的作用

一个应用开始运行以后放在那里,如果不对它进行任何操作,这个应用就像静止了一样,不会自发的有任何动作发生,但是如果我们点击界面上的一个按钮,这个时候就会有对应的按钮响应事件发生。给我们的感觉就像应用一直处于随时待命的状态,在没人操作的时候它一直在休息,在让它干活的时候,它就能立刻响应。其实,这就是run loop的功劳。

二、线程与runloop的关系

  <1>线程任务的类型

  线程的任务可以形象地分为:

    (1)直线型:执行一段任务之后,就被释放掉了。

    (2)环型:不断循环,直到通过某种方式将它终止。

  <2>线程与run loop的关系

  Run loop,正如其名,loop表示某种循环,和run放在一起就表示一直在运行着的循环。实际上,run loop和线程是紧密相连的,可以这样说run loop是为了线程而生,没有线程,它就没有存在的必要。Run loops是线程的基础架构部分,Cocoa和CoreFundation都提供了run loop对象方便配置和管理线程的run loop(以下都已Cocoa为例)。每个线程,包括程序的主线程(main thread)都有与之相应的run loop对象。

  iOS 系统中,提供了两种RunLoop:NSRunLoop 和 CFRunLoopRef。

    <1 CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。

    <2 NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。

    <3 CFRunLoopRef 的代码是开源的。

  其中:主线程中的runloop是默认启动的。

  int main(int argc, char *argv[])
 {

        @autoreleasepool {

              return UIApplicationMain(argc, argv, nil, NSStringFromClass([appDelegate class]));

        }

  }

  重点是UIApplicationMain() 函数,这个方法会为main thread 设置一个NSRunLoop 对象。这样就能解释了为什么系统没有任务执行时进行死亡状态,有任务执行时又能进行响应。

 三、RunLoop的应用场景

  1.保持线程的存活,而不是线性的执行完任务就退出了

  <1>不开启RunLoop的线程

  在遇到一些耗时操作时,为了避免主线程阻塞导致界面卡顿,影响用户体验,往往我们会把这些耗时操作放在一个临时开辟的子线程中。操作完成了,子线程线性的执行了代码也就退出了,就像下面一样。

-(void)notDidThread{
    NSLog(@"%@ -------开辟子线程",[NSThread currentThread]);
    MyThread *subThread = [[MyThread alloc]initWithTarget:self selector:@selector(subThreaddo) object:nil];
    subThread.name = @"subThread";
    [subThread start];
}
-(void)subThreaddo{
    NSLog(@"%@----执行子线程任务",[NSThread currentThread]);
}

其中MyThread是一个继承自NSThread的子类,并重写了dealloc方法。

-(void)dealloc
{
    NSLog(@"%@线程被释放了", self.name);
}

看一下打印结果:

<NSThread: 0x600001a22880>{number = 1, name = main} -------开辟子线程
<MyThread: 0x600001a42640>{number = 3, name = subThread}----执行子线程任务
subThread线程被释放了

可以看到子线程subThread在任务执行结束后,已经被释放掉了。

  <1>开启RunLoop的线程  

  (1)实验用self来持有子线程

  同样也是上个代码,让self对子线程进行持有,再看输出结果。

self.subThread = [[MyThread alloc]initWithTarget:self selector:@selector(subThreaddo) object:nil];
self.subThread.name = @"subThread";
[self.subThread start];
 <NSThread: 0x600002f9e900>{number = 1, name = main} -------开辟子线程
<MyThread: 0x600002fc2c40>{number = 3, name = subThread}----执行子线程任务

在任务执行完成之后,子线程并没有被释放掉。那既然没有被释放掉,如果再去重新开启能行吗?

self.subThread = [[MyThread alloc]initWithTarget:self selector:@selector(subThreaddo) object:nil];
self.subThread.name = @"subThread";
[self.subThread start];
[self.subThread start];//重新开启一次
<NSThread: 0x600002cb8000>{number = 1, name = main} -------开辟子线程
<MyThread: 0x600002cd5ac0>{number = 3, name = subThread}----执行子线程任务
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[MyThread start]: attempt to start the thread again'

发现已经崩溃了。任务执行完毕后,thread虽然没有被释放掉,还是处于内存中,但是它处于死亡状态(当线程执行完毕后,都会进如到这种状态),所以如果重新开启会出现崩溃。苹果在线程死亡后不允许重新开启。

  <2>初步尝试使用RunLoop

  现在我们来初步了解下RunLoop如何使用,顺便做个小测试。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSLog(@"%@----开辟子线程",[NSThread currentThread]);
    
    NSThread *subThread = [[MyThread alloc] initWithTarget:self selector:@selector(subThreadTodo) object:nil];
    subThread.name = @"subThread";
    [subThread start];
    
}
 
- (void)subThreadTodo
{
    NSLog(@"%@----开始执行子线程任务",[NSThread currentThread]);
    //获取当前子线程的RunLoop
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    //下面这一行必须加,否则RunLoop无法正常启用。我们暂时先不管这一行的意思,稍后再讲。
    [runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
    //让RunLoop跑起来
    [runLoop run];
    NSLog(@"%@----执行子线程任务结束",[NSThread currentThread]);

  查看输出结果:

<NSThread: 0x600002621400>{number = 1, name = main} -------开辟子线程
<MyThread: 0x600002677ec0>{number = 3, name = subThread}----执行子线程任务

  这里没有对线程进行引用,也没有让线程内部的任务进行显式的循环。为什么子线程的里面的任务没有执行到输出任务结束这一步,为什么子线程没有销毁?就是因为[runLoop run];这一行的存在。

  RunLoop本质就是个Event Loop的do while循环,所以运行到这一行以后子线程就一直在进行接受消息->等待->处理的循环。所以不会运行[runLoop run];之后的代码(这点需要注意,在使用RunLoop的时候如果要进行一些数据处理之类的要放在这个函数之前否则写的代码不会被执行),也就不会因为任务结束导致线程死亡进而销毁。

  <3>如何创建RunLoop?

  苹果不允许直接创建 RunLoop,它只提供了四个自动获取的函数

[NSRunLoop currentRunLoop];//获取当前线程的RunLoop
[NSRunLoop mainRunLoop];//获取主线程的RunLoop
CFRunLoopGetMain();
CFRunLoopGetCurrent();

  函数内部的逻辑大概是下面这样:

/// 全局的Dictionary,key 是 线程, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 访问 loopsDic 时的锁
static CFSpinLock_t loopsLock;
 
/// 获取一个 pthread 对应的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
    OSSpinLockLock(&loopsLock);
    
    if (!loopsDic) {
        // 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
        loopsDic = CFDictionaryCreateMutable();
        CFRunLoopRef mainLoop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
    }
    
    /// 直接从 Dictionary 里获取。
    CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
     
    if (!loop) {
        /// 取不到时,创建一个
        loop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, thread, loop);
        /// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
        _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
    }
  
    OSSpinLockUnLock(&loopsLock);
    return loop;
}
 
CFRunLoopRef CFRunLoopGetMain() {
    return _CFRunLoopGet(pthread_main_thread_np());
}
 
CFRunLoopRef CFRunLoopGetCurrent() {
    return _CFRunLoopGet(pthread_self());
}

  注:这并不是源码,而是大神为了方便我们理解,对源码进行了一些可读性优化后的结果。

  1、线程默认不开启RunLoop,为什么我们的App或者说主线程却可以一直运行而不会结束?

主线程是唯一一个例外,当App启动以后主线程会自动开启一个RunLoop来保证主线程的存活并处理各种事件。而且从上面的源代码来看,任意一个子线程的RunLoop都会保证主线程的RunLoop的存在。

  2、RunLoop能正常运行的条件是什么?

看到刚才代码中注释说暂时不管的代码,第一次接触肯定会想[runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];这一句是什么意思?为什么必须加这一句RunLoop才能正常运行?

- (void)viewDidLoad {
    [super viewDidLoad];
     
    NSLog(@"%@----开辟子线程",[NSThread currentThread]);
    
    NSThread *subThread = [[MyThread alloc] initWithTarget:self selector:@selector(subThreadTodo) object:nil];
    subThread.name = @"subThread";
    [subThread start];
  
}
 
- (void)subThreadTodo
{
    NSLog(@"%@----开始执行子线程任务",[NSThread currentThread]);
    //获取当前子线程的RunLoop
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    //注释掉下面这行和不注释掉下面这行分别运行一次
    [runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
    NSLog(@"RunLoop:%@",runLoop);
    //让RunLoop跑起来
    [runLoop run];
    NSLog(@"%@----执行子线程任务结束",[NSThread currentThread]);
}

   注释掉得到的结果

  不注释得到的结果

  注释掉以后我们看似run了RunLoop但是最后线程还是结束了任务,然后销毁了。与没注释得到的结果比较,造成这一切的原因就在上面两张图片中标注部分的区别上。要解释这一部分就又要开始讲到让我们抓耳挠腮的概念部分,我们先来看一张眼熟到不行的RunLoop结构图。

  一开始接触RunLoop我看到这张图的时候也是懵逼的,现在我们结合刚才的打印结果来理解。

  1. 图中RunLoop蓝色部分就对应我们打印结果中,整个RunLoop部分的打印结果

  2. 多个绿色部分共同被包含在RunLoop内就对应,打印结果中modes中同时包含多个Mode(这里可是看打印结果中标注出来的第一行往上再数两行。modes = ... count = 1。一个RunLoop可以包含多个Mode,每个Mode的Name不一样,只是在这个打印结果当中目前刚好Mode个数为1)

  3. 每一个绿色部分Mode整体就对应,打印结果中被标注出来的整体。

  4. 黄色部分Source对应标注部分source0+source1

  5. 黄色部分Observer对应标注部分observer部分

  6. 黄色部分Timer对应标注部分timers部分

  <1 Mode

  我对Mode的理解就是”行为模式“,就像我们说到上学这个行为模式,它就应该包含起床,出门,去学校,上课,午休等等。但是,如果上学这个行为模式什么都不包含,那么即使我们进行上学这个行为,我们也一直睡在床上什么都不会做。就像刚才注释掉addPort那一行代码得到的结果一样,RunLoop在kCFRunLoopDefaultMode下run了,但是因为该Mode下所有东西都为null(不包含任何内容),所以RunLoop什么都没做又退出来了,然后线程就结束任务最后销毁。之所以要有Mode的存在是为了让RunLoop在不同的”行为模式“之下执行不同的”动作“互不影响。比如执行上学这个行为模式就不能进行娱乐这个行为模式下的游戏这个动作。RunLoop同一时间只能运行在一种Mode下,当前运行的这个Mode叫currentMode。(这里也许比较抽象,在下面timer部分会有实例结合实例分析。)

  一般我们常用的Mode有三种

1.kCFRunLoopDefaultMode(CFRunLoop)/NSDefaultRunLoopMode(NSRunLoop)
默认模式,在RunLoop没有指定Mode的时候,默认就跑在DefaultMode下。一般情况下App都是运行在这个mode下的
 
2.(CFStringRef)UITrackingRunLoopMode(CFRunLoop)/UITrackingRunLoopMode(NSRunLoop)
一般作用于ScrollView滚动的时候的模式,保证滑动的时候不受其他事件影响。
 
3.kCFRunLoopCommonModes(CFRunLoop)/NSRunLoopCommonModes(NSRunLoop)
这个并不是某种具体的Mode,而是一种模式组合,在主线程中默认包含了NSDefaultRunLoopMode和 UITrackingRunLoopMode。子线程中只包含NSDefaultRunLoopMode。
注意:
①在选择RunLoop的runMode时不可以填这种模式否则会导致RunLoop运行不成功。
②在添加事件源的时候填写这个模式就相当于向组合中所有包含的Mode中注册了这个事件源。
③你也可以通过调用CFRunLoopAddCommonMode()方法将自定义Mode放到 kCFRunLoopCommonModes组合。

  <2 Source

  source就是输入源事件,分为source0和source1这两种。

1.source0:诸如UIEvent(触摸,滑动等),performSelector这种需要手动触发的操作。
2.source1:处理系统内核的mach_msg事件(系统内部的端口事件)。诸如唤醒RunLoop或者让RunLoop进入休眠节省资源等。
一般来说日常开发中我们需要关注的是source0,source1只需要了解。
之所以说source0更重要是因为日常开发中,我们需要对常驻线程进行操作的事件大多都是source0,稍后的实验会讲到。

  <3 Timer

  Timer即为定时源事件。通俗来讲就是我们很熟悉的NSTimer,其实NSTimer定时器的触发正是基于RunLoop运行的,所以使用NSTimer之前必须注册到RunLoop,但是RunLoop为了节省资源并不会在非常准确的时间点调用定时器,如果一个任务执行时间较长,那么当错过一个时间点后只能等到下一个时间点执行,并不会延后执行(NSTimer提供了一个tolerance属性用于设置宽容度,如果确实想要使用NSTimer并且希望尽可能的准确,则可以设置此属性)。

  <4 Observer

  它相当于消息循环中的一个监听器,随时通知外部当前RunLoop的运行状态。NSRunLoop没有相关方法,只能通过CFRunLoop相关方法创建

 // 创建observer
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
 
        NSLog(@"----监听到RunLoop状态发生改变---%zd", activity);
 
    });
 
    // 添加观察者:监听RunLoop的状态
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

  由于它与这一问的关系并不大所以暂时不做过多阐述,希望进一步了解Observer可以查看文末的文档或者RunLoop入门学习补充资料(3.Observer)。

  重点:它不能作为让RunLoop正常运行的条件,只有Observer的RunLoop也是无法正常运行的。

猜你喜欢

转载自www.cnblogs.com/xjf125/p/10148858.html