前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布

RunLoop

作者头像
陈雨尘
发布2021-02-04 09:57:58
3530
发布2021-02-04 09:57:58
举报
文章被收录于专栏:雨尘分享雨尘分享

临近春节,回望2020十分感慨,今年年初换了工作一年来都比较忙,回看上次写的文章停留在了2020年1月,上次写iOS文章停留在2018年3月十分感慨,这里总结下近期研究的RunLoop

可能很多开发者知道RunLoop,就是一个循环,但是当面试官问题RunLoop 休眠之后怎么被唤醒的?source0 和source1 都是负责什么的?AutoreleasePool是怎么做得到自动释放的?RunLoop和线程的关系是怎么维护的?等等这一系列相关问答的时候你能否回答出来,如果不能那可以继续往下看,我们一起研究下神奇的RunLoop!

1.什么是RunLoop

循环机制Event Loop :安卓的Looper, Windows 程序的消息循环,再比如 OSX/iOS 里的 RunLoop,实现这种模型的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。对于iOS来说就是 NSRunLoop 和 CFRunLoopRefCFRunLoopRef RunLoop 实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。RunLoop 在没有事件处理的时候,会使线程进入睡眠模式,从而节省 CPU 资源,提高程序性能。

2.RunLoop内部结构(组成)

在CFRunLoop源码中有5个类跟RunLoop相关 CFRunLoopRef:代表 RunLoop 的对象 CFRunLoopModeRef:代表 RunLoop 的运行模式 CFRunLoopSourceRef:就是 RunLoop 模型图中提到的输入源 / 事件源 CFRunLoopTimerRef:就是 RunLoop 模型图中提到的定时源 CFRunLoopObserverRef:观察者,能够监听 RunLoop 的状态改变

CFRunLoopRef

CFRunLoopSourceRef

source0 接收内核事件,系统事件,监听系统端口标记为待处理 但是不能直接处理事件和唤醒RunLoop

Source1 mach_port 通过内核和线程内相互发送消息,唤醒RunLoop

CFRunLoopModeRef

mode 类型一共有5种,我们要比较常用的有 kCFRunLoopDefaultMode (NSDefaultRunLoopMode) : App 默认的Model ,通常主线在这个model下运行的。 UITrackingRunLoopMode:界面更总Model,scrollow追踪触摸滑动,保证界面流畅不受其他mode 影响 kCFRunLoopCommonModes (NSRunLoopCommonModes) 占位 NSDefaultRunLoopMode + UITrackingRunLoopMode

CFRunLoopTimerRef CFRunLoopTimerRef 是基于时间的触发器,它和NSTmer 是toll-free bridged 的可以混用。 当加入到Runloop 的时候会记录对应时间点,当时间点到 就会执行对应的回调

CFRunLoopObserver

状态有 即将进入Loop,即将处理Timer,即将处理source,即将进入休眠,即将从休眠中唤醒,即将退出loop

RunLoop 的内部逻辑 (以下部分借鉴自 ibireme 大神)

根据苹果在文档里的说明,RunLoop 内部的逻辑大致如下:

其内部代码整理如下 (太长了不想看可以直接跳过去,后面会有说明):

代码语言:javascript
复制
void CFRunLoopRun(void) {
   CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}

/// 用指定的Mode启动,允许设置RunLoop超时时间
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
   return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

/// RunLoop的实现

int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
   
   /// 首先根据modeName找到对应mode
   CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
   /// 如果mode里没有source/timer/observer, 直接返回。
   if (__CFRunLoopModeIsEmpty(currentMode)) return;
   
   /// 1. 通知 Observers: RunLoop 即将进入 loop。
   __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
   
   /// 内部函数,进入loop
   __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
       
       Boolean sourceHandledThisLoop = NO;
       int retVal = 0;
       do {

           /// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
           __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
           /// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
           __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
           /// 执行被加入的block
           __CFRunLoopDoBlocks(runloop, currentMode);
           
           /// 4. RunLoop 触发 Source0 (非port) 回调。
           sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
           /// 执行被加入的block
           __CFRunLoopDoBlocks(runloop, currentMode);

           /// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
           if (__Source0DidDispatchPortLastTime) {
               Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
               if (hasMsg) goto handle_msg;
           }
           
           /// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
           if (!sourceHandledThisLoop) {
               __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
           }
           
           /// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
           /// • 一个基于 port 的Source 的事件。
           /// • 一个 Timer 到时间了
           /// • RunLoop 自身的超时时间到了
           /// • 被其他什么调用者手动唤醒
           __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
               mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
           }

           /// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
           __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
           
           /// 收到消息,处理消息。
           handle_msg:

           /// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
           if (msg_is_timer) {
               __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
           } 

           /// 9.2 如果有dispatch到main_queue的block,执行block。
           else if (msg_is_dispatch) {
               __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
           } 

           /// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
           else {
               CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
               sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
               if (sourceHandledThisLoop) {
                   mach_msg(reply, MACH_SEND_MSG, reply);
               }
           }
           
           /// 执行加入到Loop的block
           __CFRunLoopDoBlocks(runloop, currentMode);
           

           if (sourceHandledThisLoop && stopAfterHandle) {
               /// 进入loop时参数说处理完事件就返回。
               retVal = kCFRunLoopRunHandledSource;
           } else if (timeout) {
               /// 超出传入参数标记的超时时间了
               retVal = kCFRunLoopRunTimedOut;
           } else if (__CFRunLoopIsStopped(runloop)) {
               /// 被外部调用者强制停止了
               retVal = kCFRunLoopRunStopped;
           } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
               /// source/timer/observer一个都没有了
               retVal = kCFRunLoopRunFinished;
           }
           
           /// 如果没超时,mode里没空,loop也没被停止,那继续loop。
       } while (retVal == 0);
   }
   
   /// 10. 通知 Observers: RunLoop 即将退出。
   __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
} 

可以看到,实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。

RunLoop 的底层实现

从上面代码可以看到,RunLoop 的核心是基于 mach port 的,其进入休眠时调用的函数是 mach_msg()。为了解释这个逻辑,下面稍微介绍一下 OSX/iOS 的系统架构。

苹果官方将整个系统大致划分为上述4个层次: 应用层包括用户能接触到的图形应用,例如 Spotlight、Aqua、SpringBoard 等。 应用框架层即开发人员接触到的 Cocoa 等框架。 核心框架层包括各种核心框架、OpenGL 等内容。 Darwin 即操作系统的核心,包括系统内核、驱动、Shell 等内容,这一层是开源的,其所有源码都可以在 opensource.apple.com 里找到。

我们在深入看一下 Darwin 这个核心的架构:

RunLoop_4

其中,在硬件层上面的三个组成部分:Mach、BSD、IOKit (还包括一些上面没标注的内容),共同组成了 XNU 内核。 XNU 内核的内环被称作 Mach,其作为一个微内核,仅提供了诸如处理器调度、IPC (进程间通信)等非常少量的基础服务。 BSD 层可以看作围绕 Mach 层的一个外环,其提供了诸如进程管理、文件系统和网络等功能。 IOKit 层是为设备驱动提供了一个面向对象(C++)的一个框架。

Mach 本身提供的 API 非常有限,而且苹果也不鼓励使用 Mach 的 API,但是这些API非常基础,如果没有这些API的话,其他任何工作都无法实施。在 Mach 中,所有的东西都是通过自己的对象实现的,进程、线程和虚拟内存都被称为”对象”。和其他架构不同, Mach 的对象间不能直接调用,只能通过消息传递的方式实现对象间的通信。”消息”是 Mach 中最基础的概念,消息在两个端口 (port) 之间传递,这就是 Mach 的 IPC (进程间通信) 的核心。

Mach 的消息定义是在 <mach/message.h> 头文件的,很简单:

代码语言:javascript
复制
  mach_msg_header_t header;
  mach_msg_body_t body;
} mach_msg_base_t;
 
typedef struct {
  mach_msg_bits_t msgh_bits;
  mach_msg_size_t msgh_size;
  mach_port_t msgh_remote_port;
  mach_port_t msgh_local_port;
  mach_port_name_t msgh_voucher_port;
  mach_msg_id_t msgh_id;
} mach_msg_header_t;

一条 Mach 消息实际上就是一个二进制数据包 (BLOB),其头部定义了当前端口 local_port 和目标端口 remote_port, 发送和接受消息是通过同一个 API 进行的,其 option 标记了消息传递的方向:

代码语言:javascript
复制
            mach_msg_header_t *msg,
            mach_msg_option_t option,
            mach_msg_size_t send_size,
            mach_msg_size_t rcv_size,
            mach_port_name_t rcv_name,
            mach_msg_timeout_t timeout,
            mach_port_name_t notify);

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

这些概念可以参考维基百科: System_callTrap_(computing)

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

关于具体的如何利用 mach port 发送信息,可以看看 NSHipster 这一篇文章,或者这里的中文翻译 。

关于Mach的历史可以看看这篇很有趣的文章:Mac OS X 背后的故事(三)Mach 之父 Avie Tevanian

RunLoop 都用来做了什么

AutoreleasePool

NSAutoreleasePool实际上是个对象引用计数自动处理器,这里主要讲下跟RunLoop的关系. 在iOS应用启动后会注册两个Observer管理和维护AutoreleasePool。不妨在应用程序刚刚启动时打印currentRunLoop可以看到系统默认注册了很多个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,优先级最低,确保发生在所有回调操作之后。 自动释放池的创建和释放,销毁的时机如下所示

kCFRunLoopEntry; // 进入runloop之前,创建一个自动释放池 kCFRunLoopBeforeWaiting; // 休眠之前,销毁自动释放池,创建一个新的自动释放池 CFRunLoopExit; // 退出runloop之前,销毁自动释放池

页面的渲染/界面更新

当我们操作UI时,或者手动调用setNeedsDisplay/setNeedsLayout 的时候, 这等于给当前的 layer 打上了一个标记,而此时并没有直接进行绘制工作。也就是会先记录下来,等当前的 Runloop 即将休眠,也就是 beforeWaiting 时才会进行绘制工作。

响应事件

苹果注册了一个 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 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理

NSTimer

NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop 为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。 如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。 CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer相似),造成界面卡顿的感觉。在快速滑动 TableView 时,即使一帧的卡顿也会让用户有所察觉。Facebook 开源的 AsyncDisplayLink 就是为了解决界面卡顿的问题,其内部也用到了 RunLoop。

PerformSelecter

当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。 当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

网络请求

• CFSocket 是最底层的接口,只负责 socket 通信。 • CFNetwork 是基于 CFSocket 等接口的上层封装,ASIHttpRequest 工作于这一层。 • NSURLConnection 是基于 CFNetwork 的更高层的封装,提供面向对象的接口,AFNetworking 工作于这一层。 • NSURLSession 是 iOS7 中新增的接口,表面上是和 NSURLConnection 并列的,但底层仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 线程),AFNetworking2 和 Alamofire 工作于这一层。 下面主要介绍下 NSURLConnection 的工作过程。 通常使用 NSURLConnection 时,你会传入一个 Delegate,当调用了 [connection start] 后,这个 Delegate 就会不停收到事件回调。实际上,start 这个函数的内部会会获取 CurrentRunLoop,然后在其中的 DefaultMode 添加了4个 Source0 (即需要手动触发的Source)。CFMultiplexerSource 是负责各种 Delegate 回调的,CFHTTPCookieStorage 是处理各种 Cookie 的。 当开始网络传输时,我们可以看到 NSURLConnection 创建了两个新线程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader 这个线程内部会使用 RunLoop 来接收底层 socket 的事件,并通过之前添加的 Source0 通知到上层的 Delegate。

NSURLConnectionLoader 中的 RunLoop 通过一些基于 mach port 的 Source 接收来自底层 CFSocket 的通知。当收到通知后,其会在合适的时机向 CFMultiplexerSource 等 Source0 发送通知,同时唤醒 Delegate 线程的 RunLoop 来让其处理这些通知。CFMultiplexerSource 会在 Delegate 线程的 RunLoop 对 Delegate 执行实际的回调。

延时操作(如加载图片,绘制等复杂时间的处理 用到Observer)

线程

RunLoop 和线程是息息相关的,我们知道线程的作用是用来执行特定的一个或多个任务,在默认情况下,线程执行完之后就会退出,就不能再执行任务了。这时我们就需要采用一种方式来让线程能够不断地处理任务,并不退出。所以,我们就有了 RunLoop。

  1. 一条线程对应一个RunLoop对象,每条线程都有唯一一个与之对应的 RunLoop 对象。其关系维护在CFMutableDictionaryRef 的dic 中
  2. RunLoop 并不保证线程安全。我们只能在当前线程内部操作当前线程的 RunLoop 对象,而不能在当前线程内部去操作其他线程的 RunLoop 对象方法。
  3. RunLoop 对象在第一次获取 RunLoop 时创建,销毁则是在线程结束的时候。
  4. 主线程的 RunLoop 对象系统自动帮助我们创建好了,而子线程的 RunLoop对象需要我们主动创建和维护。

参考文献

官方文档 runloop绕不过的ibireme大神 视频

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.什么是RunLoop
  • 2.RunLoop内部结构(组成)
  • RunLoop 的内部逻辑 (以下部分借鉴自 ibireme 大神)
  • RunLoop 的底层实现
  • RunLoop 都用来做了什么
  • 参考文献
相关产品与服务
文件存储
文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档