美文网首页iOS实战干货
【iOS】RunLoop 知识点

【iOS】RunLoop 知识点

作者: irenb | 来源:发表于2020-09-20 20:38 被阅读0次

    一、基础篇

    1.RunLoop是什么

    • RunLoop字面意思是跑圈,实际就是运行循环(即死循环)

      其实它内部就是do-while循环,在这个循环内部不断的处理各种任务(比如Source、Timer、Observer)

    2.RunLoop基本作用

    • 保持程序持续运行(保证程序不退出)
    • 处理(监听)APP中的各种事件(如,监听触摸事件、定时器事件、Selector事件)
    • 节省CPU资源,提高程序性能(有事情时就做事情,没事情时就休息待命)

    3.获取RunLoop对象

    • Foundation框架:

      [NSRunLoop currentRunLoop];   // 获得当前线程的RunLoop对象
      [NSRunLoop mainRunLoop];  // 获得主线程的RunLoop对象
      // NSRunLoop类是OC编写的,是对CFRunLoopRef的一个简单的封装
      
    • Core Foundation框架:

      CFRunLoopGetCurrent();    // 获得当前线程的RunLoop对象
      CFRunLoopGetMain();   // 获得主线程的RunLoop对象
      // CFRunLoopRef是C语言编写的,更底层,开源
      

    二、提高篇

    1.RunLoop和线程间的关系

    • 一个线程对应一个RunLoop(key和value的关系)。

    • 线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop在第一次获取时创建,在线程结束时销毁。

      创建子线程的RunLoop直接调用 [NSRunLoop currentRunLoop];, 这个Get方法是懒加载的。

    • 主线程的RunLoop默认是自动开启,其它线程(子线程)的RunLoop需要手动开启。

      [[NSRunLoop currentRunLoop] run]; // 手动开启RunLoop

    • 分析源码:

      /**
        iOS 开发中能遇到两个线程对象: pthread_t 和 NSThread。
        可以通过 pthread_main_thread_np() 或 [NSThread mainThread] 来获取主线程;
        通过 pthread_self() 或 [NSThread currentThread] 来获取当前线程。
        CFRunLoop 是基于 pthread 来管理的。
        
        苹果不允许直接创建 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。 这两个函数内部的逻辑大概是下面这样:
      */
      
      /// 全局的Dictionary,key 是 pthread_t(线程), value 是 CFRunLoopRef(RunLoop)
      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());
      }
      

    2.RunLoop相关类

    • Core Foundation中关于RunLoop的5个类:

      CFRunLoopRef      // 获得当前RunLoop和主RunLoop
      CFRunLoopModeRef  // 代表的是RunLoop的运行模式
      CFRunLoopSourceRef    // 事件源,输入源
      CFRunLoopTimerRef     // 定时器时间
      CFRunLoopObserverRef  // 观察者,能够监听RunLoop的状态改变
      

      RunLoop的相关类之间的关系如下:

      [图片上传失败...(image-9690b1-1601390921288)]

      一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。但RunLoop每次只能选择一个模式运行。要保证运行循环RunLoop不退出,每个模式里面至少存在一个Source或者一个Timer,Observer可以有也可以没有,只是监听RunLoop的运行状态。

      CFRunLoopSourceRef 是事件产生的地方。Source有两个版本:Source0 和 Source1。

      Source0:基于用户主动触发的事件(触摸事件,performSelector 都会触发Source0事件)

      ​ 点击button 或点击屏幕,当点击屏幕时,手指和屏幕产生一个事件,这个事件会自动打包生成一个Source0事件

      Source1:基于Port的线程间通信(与内核相关,自发调用的)
      注意:Source1在处理的时候会分发一些操作给Source0去处理

      CFRunLoopTimerRef 是基于时间的触发器。

      其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。

      CFRunLoopObserverRef 是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:

      typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
          kCFRunLoopEntry         = (1UL << 0), // 即将进入运行循环
          kCFRunLoopBeforeTimers  = (1UL << 1), // 即将处理定时器事件
          kCFRunLoopBeforeSources = (1UL << 2), // 即将处理输入源事件
          kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
          kCFRunLoopAfterWaiting  = (1UL << 6), // 刚从休眠中唤醒
          kCFRunLoopExit          = (1UL << 7), // 退出运行循环
          kCFRunLoopAllActivities = 0x0FFFFFFFU // 运行循环所有活动
      };
      

      添加观察者到运行循环的代码:

      // 监听RunLoop的各种活动状态(包括唤醒,休息,以及处理各种事件等...)
      - (void)observerRunLoopActivity {
          /* 
           1.创建观察者
            参数1: 分配内存空间的方式,传默认
            参数2: RunLoop的运行状态
            参数3: 是否持续观察
            参数4: 优先级,传0
            参数5: 观察者观测到状态改变时触发的方法
           */
          CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
              switch (activity) {
                  case kCFRunLoopEntry:
                      NSLog(@"RunLoop进入");
                      break;
                  case kCFRunLoopBeforeTimers:
                      NSLog(@"RunLoop要处理定时器(Timers)事件了");
                      break;
                  case kCFRunLoopBeforeSources:
                      NSLog(@"RunLoop要处理输入源(Sources)事件了");
                      break;
                  case kCFRunLoopBeforeWaiting:
                      NSLog(@"RunLoop要休息了");
                      break;
                  case kCFRunLoopAfterWaiting:
                      NSLog(@"RunLoop醒来了");
                      break;
                  case kCFRunLoopExit:
                      NSLog(@"RunLoop退出了");
                      break;
                  default:
                      break;
              }
          });
          /* 
           2.添加观察者到运行循环
            参数1: 要监听哪个RunLoop, 传入当前的运行循环
            参数2: 观察者/监听者, 观察运行循环的各种状态
            参数3: 运行循环的模式,要监听RunLoop在哪种运行模式下的状态
           */
          CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
            /** 
             3.释放观察者 
             CF的内存管理(Core Foundation):凡是带有Create、Copy、Retain等字眼的函数,创建出来的对象,都需要在最后做一次release
            */
          CFRelease(observer);
      }
      

    3.RunLoop的model

    • RunLoop 有五种运行模式,其中我们常用的是1、2、5这三个。

      kCFRunLoopDefaultMode // 1> App的默认Mode,通常主线程是在这个Mode下运行
      UITrackingRunLoopMode // 2> 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
      UIInitializationRunLoopMode   // 3> 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
      GSEventReceiveRunLoopMode // 4> 接受系统事件的内部 Mode,通常用不到
      kCFRunLoopCommonModes     // 5> 这是一个占位用的Mode,不是一种真正的Mode
      
    • Model间的切换:

      我们平时在开发中一定遇到过,当我们使用NSTimer每一段时间执行一些事情时滑动UIScrollView,NSTimer就会暂停,当我们停止滑动以后,NSTimer又会重新恢复的情况

      // 创建定时器并添加到RunLoop
      // [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];
      
      NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];
      // 把定时器添加到RunLoop中
      // 1.NSDefaultRunLoopMode 默认运行模式,此时定时器任务只会在默认模式下执行
      [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; 
      
      // 当scrollView滑动的时候,timer失效,停止滑动时,timer恢复
      // 原因:当scrollView滑动的时候,RunLoop的Mode会自动切换成UITrackingRunLoopMode模式,因此timer失效,当停止滑动,RunLoop又会切换回NSDefaultRunLoopMode模式,因此timer又会重新启动了
      
      // 2. UITrackingRunLoopMode 界面跟踪模式,此时定时器任务只会在滑动scrollView时执行
      [[NSRunLoop mainRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
          
      // 3. 那个如何让timer在两个模式下都可以运行呢?(即滚动视图时,不会对定时器产生影响)
      // 3.1 在两个模式下都添加timer 是可以的,但是timer添加了两次,并不是同一个timer
      // 3.2 使用占位的运行模式 NSRunLoopCommonModes标记,凡是被打上NSRunLoopCommonModes标记的都可以运行,因此也就是说如果我们使用NSRunLoopCommonModes,timer可以在UITrackingRunLoopMode,kCFRunLoopDefaultMode两种模式下运行
      [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
      

      在实际开发中,一般不把timer放到主线程的RunLoop中,因为主线程在执行阻塞的任务时,timer计时会不准。
      如何让计时准确?如果timer在主线程中阻塞了怎么办?
      1》放入子线程中(即要开辟一个新的线程,但是成本是需要开辟一个新的线程)
      2》写一种跟RunLoop没有关系的计时,即GCD。(不会阻塞,推荐使用这种)

      // GCD定时器(常用)
      // 创建队列
      dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
      // 1.创建一个GCD定时器
      /*
       第一个参数:表明创建的是一个定时器
       第四个参数:队列
       */
      dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
      // 需要对timer进行强引用,保证其不会被释放掉,才会按时调用block块
      // 局部变量,让指针强引用
      self.timer = timer;
      // 2.设置定时器的开始时间,间隔时间,精准度
      /*
       第1个参数:要给哪个定时器设置
       第2个参数:开始时间
       第3个参数:间隔时间
       第4个参数:精准度 一般为0 在允许范围内增加误差可提高程序的性能
       GCD的单位是纳秒 所以要*NSEC_PER_SEC
       */
      dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
      
      // 3.设置定时器要执行的事情
      dispatch_source_set_event_handler(timer, ^{
          NSLog(@"---%@--",[NSThread currentThread]);
          // 取消定时
            if (判断条件) {
            dispatch_source_cancel(timer);
                self.timer = nil;
            }
      });
      // 4.启动
      dispatch_resume(timer);
      

    4.应用场景

    • 定时器:实例化定时器并指定监听方法后,需要把定时器加到RunLoop上。加到RunLoop上之后,才能在每个时间触发的时候去监听事件。

      self.timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];
      [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; 
      

      使用定时器还有一个细节:就是在定时器不使用的时候,必须要销毁,否则会产生循环引用。

      target后有一个self,定时器会对self强引用;viewController本身也会对定时器强引用(定时器通常会保存到viewController的实例变量/属性中),所以就会产生循环引用。

    • 常驻线程:永远活着的线程。开启一个子线程,再手动开启这个子线程的RunLoop,这个子线程就是常驻线程。

      常驻线程的生命周期跟APP相同。跟主线程并行,永远不会被销毁,一直在后台默默的做一些事情。(常驻线程的使用一般比较少,在实际开发中基本上没有这种需求)

      在子线程里面也有运行循环(RunLoop),这个运行循环(RunLoop)默认不被开启;只有我们调用它的时候才会被开启(即需要手动开启)。

      [图片上传失败...(image-98d89b-1601390921288)]

      特别注意:

      在启动RunLoop之前建议用 @autoreleasepool {…}包裹。

      意义:创建一个大释放池,释放{}期间创建的临时对象,一般好的框架的作者都会这么做。

      - (void)executeTask {
          @autoreleasepool {
              NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
              [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
              [[NSRunLoop currentRunLoop] run];
          }
      }
      
    • 自动释放池

      Q:autoreleasePool对象是什么时候释放的?
      A:自动释放池的释放和创建与RunLoop有关。
        当RunLoop开启时,就会自动创建一个自动释放池。
        当Runloop准备休眠的时候,会释放旧的autoreleasePool对象,再重新创建一个新的空的autoreleasePool对象。
        当RunLoop从休眠中被唤醒的时候,Timer,Source等新的事件就会放到新的自动释放池中。
        当Runloop即将退出的时候,会释放掉相关所有的autoreleasePool对象。
      

      注意:只有主线程的RunLoop会默认启动。也就意味着会自动创建自动释放池,子线程需要在线程调度方法中手动添加自动释放池。

    • performSelector方法

      performSelector其实是创建了一个Timer,然后添加到当前的线程中。如果当前线程没有Runloop,这个方法则走不通的。

      // 可以设置只在某个运行模式(modes)下执行方法(aSelector)
      - (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes;
      - (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
      
    • 用于Socket开发:使用RunLoop能够监听网络端口数据的接收与发送情况。(平常企业开发中用Socket开发比较少,通常是做硬件通讯的时候,用得比较多。比如:智能家居开发、游戏机等)

    • iOS中默认开启的事件循环,保证主线程不退出。

      [图片上传失败...(image-5ddd5e-1601390921288)]

      第14行代码的UIApplicationMain函数内部就启动了一个RunLoop

      所以UIApplicationMain函数一直没有返回,保持了程序的持续运行

      这个默认启动的RunLoop是跟主线程相关联的。

    三、总结

    • RunLoop知识点的大致框架:

      [图片上传失败...(image-b32310-1601390921288)]

    • 思考:以后为了增加用户体验,在用户UI交互的时候不做事件处理,我们可以把需要做的操作放到NSDefaultRunLoopMode。

    • RunLoop处理逻辑流程图:

      [图片上传失败...(image-6a5199-1601390921288)]

    在实际中的使用场景其实很明确了, 在程序中中有大量临时变量(循环/遍历中)的时候最好手动创建autoreleasepool{}

    四、面试题

    1. 讲讲 RunLoop,项目中有用到吗?
    2. RunLoop内部实现逻辑?
    3. Runloop和线程的关系?
    4. timer 与 Runloop 的关系?
    5. 程序中添加每3秒响应一次的NSTimer,当拖动tableview时timer可能无法响应要怎么解决?
    6. Runloop 是怎么响应用户操作的, 具体流程是什么样的?
    7. 说说RunLoop的几种状态?
    8. Runloop的mode作用是什么?

    相关文章

      网友评论

        本文标题:【iOS】RunLoop 知识点

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