[iOS面试]第7章 RunLoop相关面试问题

本文主讲RunLoop相关面试问题,包括RunLoop概念、数据结构、事件循环机制、RunLoop与NSTimer、RunLoop与多线程。

一、RunLoop概念

1、 问题:什么是RunLoop?

答:RunLoop是通过内部维护的事件循环来对事件/消息进行管理的一个对象。
1、没有消息需要处理时,休眠以避免资源占有。
2、有消息需要处理时,立刻被唤醒。

2、EventLoop

没有消息需要被处理时, 系统会将当前线程所有权转化为内核态, 当有消息需要处理时, 系统会将当前线程的状态切换回用户态.
所以RunLoop的循环并不是一个单纯的死循环, 而是通过状态切换, 达到没有消息时休眠, 有消息时唤醒的这样一个事件循环机制.

EventLoop

在没有消息处理时,休眠以避免资源占用,它的状态切换是怎么样的?
  • 没有消息处理 是从用户态通过系统调用进入内核态.
    也就是当没有消息要处理时,进程或者说线程会进入一个休眠状态,而休眠状态的一个过渡相当于是把当前线程的控制权转移给了内核态

  • 当有消息需要处理时就会被立刻唤醒
    实际上就是由内核态用户态的一个状态切换

用户态和内核态的介绍:
  • 应用程序一般都是运行在用户态上面,也就是用户进程包括开发所使用的绝大多数的 API 都是针对于用户层面的.
  • 而当发生了系统调用,需要使用一些关于操作系统,以及一些底层内核相关的一些指令或者 API 的话,就触发了系统调用,而有些系统调用就会发生状态空间的切换,这种切换空间或者说之所以区分用户态内核态实际上是对·计算机的一些资源调度·,包括资源管理进行一个统一或者说一致性的操作,这样的话就可以合理的安排资源调度,包括可以避免一些特殊的异常
  • 比如说在内核态往往有一些线性指令,中断,包括一些开机关机的一些操作,如果说每一个用户进程可以假想是一个 app,每一个 app 都可以促使当前用户手机关机或者说中断,这种场景是无法想象的,所以要有一个用户态到内核态上面的一个区分,同时内核态里面的一些内容可以对用户态当中的一些线程进行调度和管理包括进程间的一些通信

问题:main函数为什么能保证一直运行状态不退出?

答:

  • 在main函数中调用UIApplicationMain()函数, 这个函数内部会启动一个主线程的RunLoop
  • RunLoop是对事件循环的维护机制, 可以不断的接收消息,比如说点击屏幕的事件,滑动列表,及处理网络请求的返回,那么接收消息之后对这个事件进行处理,处理完之后就会再进行等待, 通过用户态到内核态的切换, 从而避免资源占用, 让当前线程处于休眠状态.
  • 注意:等待 != 死循环 ,RunLoop的循环通过状态切换, 达到没有消息时休眠,用户态切换到内核态,有消息时唤醒,内核态切换到用户态的这样一个事件循环机制
什么是事件循环,事件循环的机制是怎样的?
  • 维护的事件循环可以用来不断的处理消息或者说事件,对他们进行管理
  • 同时当没有消息需要管理时用从用户态切换到内核态,由此可以用来进行当前线程的休眠,然后避免资源占用
  • 同时当有消息需要处理时,会发生从内核态到用户态的切换,然后当前的用户线程会被唤醒
  • 所以状态的切换才是 RunLoop 的关键点

二、RunLoop的数据结构

RunLoop开源代码地址
在 OC 中实际提供了两个 RunLoop 的。
一个是 NSRunLoop,一个是 CFRunLoop。
NSRunLoop 是对 CFRunLoop 的封装,提供了一些面向对象的 API。
NSRunLoop 是位于 Foundation 当中的,CFRunLoop 位于 CoreFoundation 当中的。

RunLoop 的数据结构主要有三个:

  • CFRunLoop
  • CFRunLoopMode
  • Source/Timer/Observer

####1、CFRunLoop

  • CFRunLoop数据结构 由五部分组成: pthread 、currentMode、modes、commonModes、commonModeItems
    CFRunLoop数据结构

    //源码
    struct __CFRunLoop {
      CFMutableSetRef _commonModes;     // Set
      CFMutableSetRef _commonModeItems; // Set
      CFRunLoopModeRef _currentMode;    // Current Runloop Mode
      CFMutableSetRef _modes;           // Set
      ...
    };
    
  • (1)、pthread:一一对应(RunLoop和线程的关系)

  • (2)、currentMode:CFRunLoopMode
  • (3)、modes:NSMutableSet<CFRunLoopMode*>
  • (4)、commonModes:NSMutableSet<NSString*>
    1>、commonMode不是实际存在的一种Mode。
    2>、是同步Source/Timer/Observer到多个Mode中的一种技术方案。
    
  • (5)、commonModeItems:包含Observer、Timer、Source

2、CFRunLoopMode数据结构

CFRunLoopMode数据结构

//源码
struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
    CFMutableSetRef _sources0;    // Set
    CFMutableSetRef _sources1;    // Set
    CFMutableArrayRef _observers; // Array
    CFMutableArrayRef _timers;    // Array
    ...
};
  • name: 对应某一个runloopMode名称(如 NSDefaultRunLoopMode)
  • sources0: MutableSet 集合类型数据结构
  • sources1: MutableSet 集合类型数据结构
  • observers: MutableArray
  • timers: MutableArray

3、Source/Timer/Observer

  • 在 CF 框架当中官方名称叫 CFRunLoopSource ,有两种 source0 和 source1
    唤醒线程就是从内核态切换到用户态
    1>、CFRunLoopSource
    source0:需要手动唤醒线程。
    source1:具备唤醒线程的能力。

source0: 非系统事件.
只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。

source1 : 系统事件
包含了一个 mach_port和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程

2>、CFRunLoopTimer

基于事件的定时器,和NSTimer是toll-free bridged的。和平时所使用的 NSTimer 是具备免费桥转换的

3>、CFRunLoopObserver

某个observer可以监听runloop的状态变化,并作出一定反应。

观测时间点

  • KCFRunLoopEntry (RunLoop入口时机)
  • KCFRunLoopBeforeTimers (通知观察者RunLoop将要对timer一些相关事件进行处理)
  • KCFRunLoopBeforeSources (将要对处理一些sources事件)
  • KCFRunLoopBeforeWaiting (通知观察者RunLoop将要进入休眠状态, 即将要发用户态到内核态切换)
  • KCFRunLoopAfterWaiting (内核态到用户态切换不久)
  • KCFRunLoopExit (RunLoop退出通知)

可以通过注册一些 Observer 来实现对 RunLoop 的一些相关时间点的监测或者观察。

4、各个数据结构之间关系

数据结构之间关系
问题:RunLoop 和Mode以及 Mode和其对应的 Source ,Timer , Observer 有什么关系?
答:

  • 一个runLoop可以有多个mode, 一对多关系
  • mode 和Source, Timer, Observer 是一对多的关系
    可以看到, 一个RunLoop可以有多个Mode, 而每个Mode中又可以存放多个不同的事件, 我们在切换Mode时, 其他Mode的事件将不会被响应.

5、RunLoop的Mode

image.png

Runloop Mode 实际上是 Source,Timer 和 Observer 的集合,不同的 Mode 把不同组的 Source,Timer 和 Observer 隔绝开来。Runloop 在某个时刻只能跑在一个 Mode 下,处理这一个 Mode 当中的 Source,Timer 和 Observer。

苹果文档中提到的 Mode 有五个,分别是:

  • NSDefaultRunLoopMode:默认的mode,正常情况下都是在这个mode
  • NSConnectionReplyMode
  • NSModalPanelRunLoopMode
  • NSEventTrackingRunLoopMode:使用这个Mode去跟踪来自用户交互的事件(比如UITableView上下滑动)
  • NSRunLoopCommonModes

iOS 中公开暴露出来的只有 NSDefaultRunLoopModeNSRunLoopCommonModes。 NSRunLoopCommonModes 实际上是一个 Mode 的集合,默认包括 NSDefaultRunLoopMode 和 NSEventTrackingRunLoopMode。

问题:RunLoop为什么会有多个mode?
答:

6、CommonModes的特殊性

NSRunLoopCommonModes
(问题:CommonModes是否使用过, 你对CommonModes事怎样理解?)

  • CommonModes不是实际存在的一种Mode
  • 是同步Source/Timer/Observer到多个Mode中的一种技术方案

在 OC 当中经常会通过 NSRunLoopCommonModes 字符串常量来表达 CommonMode。

  • CommonModes实现:一个 Mode 可以将自己标记为”Common”属性(通过将其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 标记的所有Mode里。

三、RunLoop事件循环机制的实现

在开发过程中调用的 NSRunLoop 的 run 系列的相关方法以及 CFRunLoop 的相关的 run 方法最终都会调用到 CFRunLoopRun() 函数

1 事件循环的整体逻辑:

1)在 RunLoop 启动之后首先会发出一条通知来告诉观察者当前 RunLoop 即将启动
2)之后 RunLoop 将要处理 Timer/Sources0 事件,发出通知
3)进入正式 Sources0 的处理
4)如果有 Sources1 需要处理,这个时候会用过一条 goto 语句来进行代码逻辑的跳转,来处理唤醒时收到的消息
5)如果没有 Source1要处理,此时线程将要休眠,同时也会发送通知给 Observe,然后就发生了用户带到内核态的切换
6)线程正式进入休眠,等待唤醒
7)之后线程被唤醒,也要发送一个通知,通知观察着说当前线程被唤醒了,然后处理唤醒时受到的消息,之后又会回到第二步

2 当处于休眠的runloop,可以通过哪些方式唤醒?

答:

  • 通过 Sources1 进行当前 RunLoop 的唤醒
  • Timer 事件的回调
  • 外部手动的唤醒

3 App 从启动到退出,这个过程当中系统都发生了什么?

答:

  • 调用了 main 函数之后,在 main 函数中会调用 UIApplicationMain 函数,在这个函数内部会启动一个主线程的 RunLoop,然后经过一系列的处理最终主线程的 RunLoop 处于休眠状态,
  • 如果说此时点击一个屏幕,会产生一个 Responder ,然后基于 Responder 最终会转成 Sources1,可以把主线程唤醒,运行然后处理,
  • 之后当把程序杀死的时候 RunLoop 就会退出,这个时候就会发出一个通知即将退出 RunLoop ,RunLoop 退出之后线程也就销毁掉了。

4 RunLoop的核心


main 函数经过一系列的处理之后,内部最终会调用一个系统函数 mach_msg() ,于是就发生了一个系统调用,经过系统调用当前用户线程就把控制权转交核心态,然后 mach_msg() 在一定条件下会返回给调用方,触发返回的逻辑就是唤醒线程的逻辑,比如收到了一个 Sources1 或者 Timer 事件的回调,包括外部手动唤醒,就可以触发核心态到用户态的切换,那么当前app的主线程循环就会被唤醒,这就是 RunLoop 的核心

四、RunLoop与NSTimer

问题:滑动TableView的时候,我们的定时器还会生效吗?
答:

  • Runloop Mode 实际上是 Source,Timer 和 Observer 的集合,不同的 Mode 把不同组的 Source,Timer 和 Observer 隔绝开来。Runloop 在某个时刻只能跑在一个 Mode 下,处理这一个 Mode 当中的 Source,Timer 和 Observer。

  • 滑动TableView的时候Mode发生切换,从KCFRunLoopDefaultMode切换到UITrackingRunLoopMode. (当把Source/Timer/Observer 添加到某个Mode上, 如果当前runloop运行在另一个mode上, 对应Source/Timer/Observer是没有办法进行后续处理和回调.)

  • 解决: 通过函数CFRunLoopAddTimer() 将timer添加到commonMode上.

  • CommonMode不是实际存在的一种Mode, 只是将一些mode打上common标记, 然后可以把某个事件源(如Timer)同步到多个mode中.
    void CFRunLoopAddTimer(runloop, timer, commonMode)
    `
    

五、RunLoop与多线程

1、怎样实现一个常驻线程?

(1)、为当前线程开启一个RunLoop。
(2)、向该RunLoop中添加一个Port/Source等维持RunLoop的事件循环。
(3)、启动该RunLoop。

实现一个常驻线程的基本步骤:

  • 1)为当前线程开启一个 RunLoop :NSRunLoop *runLoop = [NSRunLoop currentRunLoop];,因为获取当前 RunLoop 这个方法本身会查找如果当前线程没有 RunLoop 的话,会在系统的内部创建
  • 2)如果线程没有资源或者事件源要处理的话,默认情况下是不能维持事件循环的就会直接退出了,所以需要给他添加一个 Port/Source 来维持他的时间循环机制
  • 3)然后再调用 RunLoop 的 run 方法就可以实现一个常驻线程

2、RunLoop和线程关系

(1)、runloop与线程是一一对应的,一个runloop对应一个核心的线程,为什么说是核心的,是因为runloop是可以嵌套的,但是核心的只能有一个,他们的关系保存在一个全局的字典里。
(2)、runloop是来管理线程的,当线程的runloop被开启后,线程会在执行完任务后进入休眠状态,有了任务就会被唤醒去执行任务。
(3)、runloop在第一次获取时被创建,在线程结束时被销毁。
(4)、对于主线程来说,runloop在程序一启动就默认创建好了。
(5)、对于子线程来说,runloop是懒加载的,只有当我们使用的时候才会创建。

    dispatch_queue_t queue = dispatch_queue_create("com.codeTao.testQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{
        [self performSelector:@selector(timerAction) withObject:nil afterDelay:1];
        [[NSRunLoop currentRunLoop] run];
    });

(6)、子线程中使用定时器,需将定时器添加至RunLoop中,确保子线程的runloop被创建,不然定时器不会回调。

  dispatch_queue_t queue = dispatch_queue_create("com.codeTao.testQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{
        //此种方式创建的timer已经添加到NSRunloop中了
        NSTimer *timer1 =[NSTimer scheduledTimerWithTimeInterval:0 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] run];

        //此种方式创建的timer没有添加至runloop中
        NSTimer *timer2 = [NSTimer timerWithTimeInterval:1.0f target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:timer2 forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] run];
    });

六 RunLoop面试总结

问题:什么是RunLoop? 它是怎样做到有事做事,没事休息?

答: RunLoop是通过内部维护的事件循环来对事件/消息进行管理的一个对象。
调用 [CFRunLoop run] 相关方法后, 会调用系统函数mach_msg, 同时发生了用户态向核心态的切换,当前线程处于休眠状态.所有做到有事做事,没事休息

问题:RunLoop与线程是怎样的关系?

答:

  • runloop与线程是一一对应的
  • 一个线程默认是没有runloop的, 需要手动创建

问题:如何实现一个常驻线程?

答:三个步骤:
(1)、创建一个线程对应的RunLoop。
(2)、向该RunLoop中添加一个Port/Source/Timer/Observer等维持RunLoop的事件循环。
(3)、启动该RunLoop 。调用CFRunLoop run方法
注意:运行的模式和添加模式必须是同一个,否则外部使用while循环会导致死循环

问题:怎样保证子线程数据回来更新UI的时候不打断用户的滑动操作?

答:

  • 用户进行滑动过程中,当前RunLoop运行在UITrackingRunLoopMode下
  • 我们一般在子线程中进行网络请求, 所以可以将子线程抛给主线程数据并进行UI更新的逻辑封装起来提交到主线程的NSDefaultRunLoopMode下.
  • 这样抛回来的任务,当用户滑动时处于UITrackingRunLoopMode下就不会执行任务. 当手停止滑动操作后, 当前线程mode切换到NSDefaultRunLoopMode下,再处理子线程上抛给主线程的任务,这样就不会打断用户滑动操作.

问题:我们可以监听 RunLoop 哪些时间点?

答:

  • KCFRunLoopEntryRunLoop 的入口时机,当 RunLoop 准备启动的时候系统会给我们一个回调通知,这个通知掉 CFRunLoopEntry
  • KCFRunLoopBeforeTimers代表的含义:通知观察者 RunLoop 将要对 Timer 一些相关事件进行处理了
  • KCFRunLoopBeforeSources代表将要处理一些 Source 事件
  • KCFRunLoopBeforeWaiting通知对应观察者,当前 RunLoop 将要进入休眠状态,这个通知或者说观测点是非常重要的一个观测点,在 RunLoop 发送这个通知的时候,即将要发生用户态到内核态的切换
  • KCFRunLoopAfterWaiting这也是一个重要的观测点,这个通知发出的时机恰好是从内核态切换到用户态之后的不久之间
  • KCFRunLoopExit代表 RunLoop 退出的通知

问题:什么时候使用Runloop?
当需要和该线程进行交互的时候才会使用Runloop

问题: Runloop和线程是什么关系?
答:

  • 每条线程都有唯一的一个与之对应的RunLoop对象,其关系是保存在一个全局的 Dictionary 里;
  • 主线程的RunLoop已经自动创建,子线程的RunLoop需要手动创建;
  • RunLoop在第一次获取时创建,在线程结束时销毁

问题:Runloop的mode作用是什么?
答:
指定事件在运行循环中的优先级的,
线程的运行需要不同的模式,去响应各种不同的事件,去处理不同情境模式。(比如可以优化tableview的时候可以设置UITrackingRunLoopMode下不进行一些操作,比如设置图片等。)

问题: 以+scheduledTimerWithTimeInterval:的方式触发的timer,在滑动页面上的列表时,timer会暂停回调, 为什么?
答:
滑动scrollView时,主线程的RunLoop会切换到UITrackingRunLoopMode这个Mode,执行的也是UITrackingRunLoopMode下的任务(Mode中的item),而timer是添加在NSDefaultRunLoopMode下的,所以timer任务并不会执行,只有当UITrackingRunLoopMode的任务执行完毕,runloop切换到NSDefaultRunLoopMode后,才会继续执行timer。

问题: 如何解决在滑动页面上的列表时,timer会暂停回调?
答:
Timer放到NSRunLoopCommonModes中执行即可

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
[[NSRunLoop currentRunLoop] run];

问题: NSTimer使用时需要注意什么?
答:

  • 注意timer添加到runloop时应该设置为什么mode
  • 注意timer在不需要时,一定要调用invalidate方法使定时器失效,否则得不到释放

问题: RunLoop 有哪些应用?
答: 常驻内存、AutoreleasePool 自动释放池

问题: AutoreleasePool 和 RunLoop 有什么联系?
答:
iOS应用启动后会注册两个 Observer 管理和维护 AutoreleasePool。应用程序刚刚启动时默认注册了很多个Observer,其中有两个Observer的 callout 都是 _ wrapRunLoopWithAutoreleasePoolHandler,这两个是和自动释放池相关的两个监听。

  • 第一个 Observer 会监听 RunLoop 的进入,它会回调objc_autoreleasePoolPush() 向当前的 AutoreleasePoolPage 增加一个哨兵对象标志创建自动释放池。这个 Observer 的 order 是 -2147483647 优先级最高,确保发生在所有回调操作之前。

  • 第二个 Observer 会监听 RunLoop 的进入休眠和即将退出 RunLoop 两种状态,在即将进入休眠时会调用 objc_autoreleasePoolPop() 和 objc_autoreleasePoolPush() 根据情况从最新加入的对象一直往前清理直到遇到哨兵对象。而在即将退出 RunLoop 时会调用objc_autoreleasePoolPop() 释放自动自动释放池内对象。这个Observer 的 order 是 2147483647 ,优先级最低,确保发生在所有回调操作之后。

问题:NSRunLoop 和 CFRunLoopRef 区别?
答: CFRunLoopRef 基于C 线程安全,NSRunLoop 基于 CFRunLoopRef 面向对象的API 是不安全的