当面试官问Runloop时,想听到的答案是什么?

·  阅读 10962

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天 点击查看活动详情

Runloop这个算是iOS开发者面试中最常见的问题之一了把,但是每次面试遇到这种问题都会菊花一紧,生怕回答的少了,同时也怕回答的不够全面。所以针对这一问题,总结了一下几个级别的开发者需要知道点进行归纳。

初级(0-3年)

如果你要去面试初级,以现在的内卷的程度,这个问题基本上是跑不了的,想当年作者还是初级的时候,连runloop都不知道是什么。。。

Runloop 是什么

Runloop是通过内部维护的事件循环来对事件/消息进行管理的一个对象

这里有两个重点

  1. 事件循环

  2. 事件/消息进行管理


什么是事件循环呢?

事件循环(状态切换)

 没有消息需要处理时,休眠以避免资源占用

            用户态——>内核态

有消息需要处理时,立刻被唤醒

            用户态<—— 内核态
            
            
什么是事件/消息进行管理呢?

RunLoop 通过 mach_msg()函数接收、发送消息来进行管理。
它的本质是调用函数 mach_msg_trap(),相当于是一个系统调用,会触发内核状态切换。
可以做到在有事做的时候做事,没事做的时候,会由用户态切换到内核态,避免资源浪费。


如何实现事件、消息的管理

mach_msg() 函数实际上是调用了一个 Mach 陷阱 (trap),
即函数mach_msg_trap(),陷阱这个概念在 Mach 中等同于系统调用。
当你在用户态调用 mach_msg_trap() 时会触发陷阱机制,切换到内核态;
内核态中内核实现的 mach_msg() 函数会完成实际的工作,

截屏2022-03-15 上午9.53.01.png

所以说 Runloop的核心就是一个 mach_msg(),RunLoop 调用这个函数去接收消息,如果没有别人发送 port 消息过来,内核会将线程置于等待状态。例如你在模拟器里跑起一个 iOS 的 App,然后在 App 静止时点击暂停,你会看到主线程调用栈是停留在 mach_msg_trap() 这个地方。

说了这么多,应该可以对初级iOS开发者提供一个比较系统的概念,如果你还想卷的话,可以看继续往下看

中级(3-6年)

在这级别里,需要知道的就不仅仅是Runloop是什么了,更应该知道其中的数据结构和实际使用。

ps:卷的不能再卷了(╬ Ò ‸ Ó)

Runloop的数据结构

NSRunloop是CFRunloop的封装,提供了面向对象的API

typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
    //...
    CFStringRef _name;
    //...
    CFMutableSetRef _sources0; // <Set>
    CFMutableSetRef _sources1;// <Set>
    CFMutableArrayRef _observers; // <Array>
    CFMutableArrayRef _timers; // <Array>
    //...
};
///CFRunLoop.h 类型重命名
typedef struct __CFRunLoop * CFRunLoopRef;
///CFRunLoop.c 结构体
struct __CFRunLoop {
    //..
    CFMutableSetRef _commonModes; // <Set> String UITrackingRunLoopMode/kCFRunLoopDefaultMode
    CFMutableSetRef _commonModeItems;// <Set> observer/source/timer
    CFRunLoopModeRef _currentMode; //当前运行的mode
    CFMutableSetRef _modes; //内置的modes;
    //...
};

CFRunloop

  • pthread (与线程一一对应)
  • currentMode (当前的运行模式)
  • modes (集合 NSMutableSet <CFRunLoopMode *>)
  • commonModes(一个存储了被标记为common modes的模式集合 【NSMutableSet <NSString *>】)
  • commonModeItems(它是一个集合, 集合里面有多个元素, 有多个Observer ,有多个Timer ,有多个Source
pthread

C语言线程对象

Runloop 和线程的关系是, 一一对应

currentMode

Runloop当前所处的模式 ,是CFRunLoopMode数据结构

modes
  • 一个Runloop有一个modes

  • 一个modes 是由多个CFRunLoopMode组成

  • 一个CFRunLoopMode是由 name/Source/Timer/Observer 组成

  • RunLoop 只能运行一个 Mode,RunLoop 只会处理它当前 Mode 的事件。

截屏2022-04-02 下午2.23.50.png

截屏2022-03-14 下午1.43.16.png

source -- CFRunLoopSourceRef

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

  • Source1 :基于mach_Port的,来自系统内核或者其他进程或线程的事件,可以主动唤醒休眠中的RunLoop(iOS里进程间通信开发过程中我们一般不主动使用)。mach_port大家就理解成进程间相互发送消息的一种机制就好。

简单举个例子:一个APP在前台静止着,此时,用户用手指点击了一下APP界面,那么过程就是下面这样的:

我们触摸屏幕,先摸到硬件(屏幕),屏幕表面的事件会先包装成Event,Event先告诉source(mach_port),source1唤醒RunLoop,然后将事件Event分发给source0,然后由source0来处理。

如果没有事件,也没有timer,则runloop就会睡眠,如果有,则runloop就会被唤醒,然后跑一圈。

Observer -- CFRunloopObserver

观察者可观测的时间点

  • kCFRunloopEntry (runloop准备启动)
  • kCFRunloopBeforeTimers (通知观察者,runloop将要对Timer的一些相关事件进行处理了)
  • kCFRunloopBeforeSources (将要处理一些Sources事件)
  • kCFRunloopBeforeWaiting( 即将要发生用户态到内核态的切换 用户态 —> 内核态)
  • kCFRunloopAfterWaiting (内核态---转--->用户态)
  • kCFRunloopExit  (runloop退出通知)

这些可观察的时间点有时也可作为检测app卡顿的功能,但是实际使用也不太好用,这里就不展开细说了。

Timer -- CFRunloopTimer

是基于时间的触发器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。

这个只做简单的了解,中级还用不到。。。

commonModes

在这里要先说一下 系统的提供的几个默认模式

- kCFRunLoopDefaultMode, App的默认运行模式,通常主线程是在这个运行模式下运行
- UITrackingRunLoopMode, 跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响)
- kCFRunLoopCommonModes, 伪模式,不是一种真正的运行模式
- UIInitializationRunLoopMode:在刚启动App时第进入的第一个Mode,启动完成后就不再使用
- GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到

UIInitializationRunLoopModeGSEventReceiveRunLoopMode 平时用不到,这里就不说了

  • kCFRunLoopDefaultMode:一般app啥都不操作时,currentMode 就是 kCFRunLoopDefaultMode。

  • UITrackingRunLoopMode:当发生app与用户进行交互时,就会退出之前的mode,进入这个mode

  • kCFRunLoopCommonModes:用人话说就是一个标记mode,当某个mode打上NSRunLoopCommonModes标记时,就会是这个

再说回commonModes、它是一个集合,里面存储着被标记kCFRunLoopCommonModes的mode的name

我们来看看commonModes的数据结构

image.png

从图片可以看出,系统默认是将kCFRunLoopDefaultModeUITrackingRunLoopMode都放到commonModes中了.

所以说 某个mode可以将自己标记为为common,标记为 common 的 mode,都会被添加进 commonModes中.

commonModeItems

当前运行在commonModes模式下的CFRunLoopSourceCFRunLoopObserverCFRunLoopTimer

理解起来就是 kCFRunLoopDefaultMode 的 source1/timer/observers 和 UITrackingRunLoopMode 的 source1/timer/observers 都包含在 commonModeItems 里面

commonModes 里每多一个 mode ,commonModeItems 里就会多一组 source1/timer/observers 。

事件循环的时间机制

我们通过一张图片来了解一下 runloop 是如何进行事件循环的

截屏2022-03-14 下午2.16.59.png

  1. main函数—> UIApplicationMain
  2. 在UIApplicationMain中启动主线程的Runloop
  3. 即将进入Runloop(通知observer)
  4. 将要处理timer、source0事件(通知observer)
  5. 处理source0事件
  6. 如果有source1事件要处理(跳转到10)
  7. 线程将要休眠(通知observer)
  8. 休眠、等待唤醒(唤醒的方法:1 source1事件,2 Timer事件,3 外部手动唤醒)
  9. 线程刚被唤醒(通知observer)
  10. 处理唤醒时收到的消息
  11. 即将退出Runloop(通知observer)

高级(6年以上)

身为高级iOSer,需要知道应该就不仅仅是数据结构了,更应该知道是如何实现的,以及项目中在哪些方面有应用。

ps 卷王就是你了 (╯‵□′)╯︵┻━┻

mode是如何切换的

首先我们来说是,mode是如何切换的 例如:scrollView 由静止到滑动,是如何由NSDefaultRunLoopMode变为UITrackingRunLoopMode

首先 我们要了解一下 CFRunLoopRunSpecific

CFRunLoopRunSpecific 是启动 Runloop 和指定 Runloop 在那个mode下执行的mode。这个函数一般是操作系统进行mode的切换。

比如滑动的时候,Runloop 会进入进入 UITrackingRunLoopMode,而app启动的时候UIInitializationRunLoopMode

每一个mode处理完成后,如果runloop没有退出,就会返回之前的mode,初始mode是default。

CFRunLoopRunSpecific 会保持前一次mode的状态属性(stopped和currentmode)然后发出即将要进入新的mode通知,然后进入__CFRunLoopRun(__CFRunLoopRun会创建一个循环),然后这个mode运行结束后再发已退出mode通知。再恢复前一次的 stopped 和 currentmode

commonMode 的作用

在中级篇里,其实我们只是了解了 commonMode 的数据结构,但是还是没有完全了解 他们的的作用是什么。

所以我们做已经烂大街的例子来说明一下,就是NSTimer定时器的添加

众所周知,NSTimer的创建,无论是通过init方式还是block的方式,如果不使用

[runLoop addTimer:timer forMode:xxx];

方法, timer会默认加入currentMode,也就是 kCFRunLoopDefaultMode

所以就会有一个问题,在一个 Tableview 上加入 NSTimer,当 Tableview 滚动时,timer 的计时就不会生效。

原因就是 Tableview 的滚动 会让 runloop 切换为 UITrackingRunLoopMode,而 timer 没有加在 UITrackingRunLoopMode 里面,所以这个timer 不会执行。

那怎么处理比较好呢?就是把 timer 关联到 NSRunLoopCommonModes 中,这样,timer 就可以在 tableview 静止或滚动时,都会执行了。

看完这个问题,就会有疑问,为什么关联到NSRunLoopCommonModes 就会都执行呢?

理解起来就是 将这个 timer 打上一个 Common 的标记。所有在 CommonModes 里的mode 都会去执行 打了 common 标记的事件。正常的时候 CommonModes只有 UITrackingRunLoopModekCFRunLoopDefaultMode,所以在这两个mode下,都会去执行 timer 事件。

Run Loop并不是在运行在 NSRunLoopCommonModes,因为NSRunLoopCommonModes 是个Mode集合,而不是一个具体的Mode。我们可以在添加事件源的时候使用 NSRunLoopCommonModes,只要Run Loop运行在NSRunLoopCommonModes中任何一个Mode,这个事件源都可以被触发

ps:`CFRunLoopAddCommonMode` 方法可以向Common Modes中添加自定义mode。这里就不展开了

具体是如何通过代码同步的,就涉及到源码了,这里就不展开说明了,(毕竟是资深才能涉猎的。。。)

下面来说说 Runloop的使用场景

AutoreleasePool

App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()

第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。

事件响应

苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。

当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。这个过程的详细情况可以参考这里。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。

_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。

手势识别

当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。

苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。

当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

界面更新

当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。

苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数: _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

按钮点击

首先是由那个Source1 接收IOHIDEvent,之后在回调 __IOHIDEventSystemClientQueueCallback() 内触发的 Source0,Source0 再触发的 _UIApplicationHandleEventQueue()。所以UIButton事件看到是在 Source0 内的。

截屏2022-03-15 上午10.58.44.png

结语

以上,便是Runloop的冰山一角,很多知识点都是简单的说了数据结构和作用,作者也只能管中窥豹一般的去用自己的理解和认知去解释,希望大家能海涵,如果有说的不清楚或者不明白的,可以联系我,虽然我也不一定知道Ψ( ̄(エ) ̄)Ψ

分类:
iOS
标签:
分类:
iOS
标签:
收藏成功!
已添加到「」, 点击更改