iOS - RunLoop 基本原理介绍

news2024/11/13 10:52:17

一、Runloop 简介

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

事件循环(状态切换)

  • 没有消息需要处理时,休眠以避免资源占用(用户态 -> 内核态)
  • 有消息需要处理时,立刻被唤醒(内核态 -> 用户态)

事件/消息管理:Runloop 通过 mach_msg() 函数接收、发送消息来进行管理。
请添加图片描述

二、Runloop 数据结构

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

typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
	// ...
    CFStringRef _name;
    // ...
    CFMutableSetRef _sources0; // <set>
    CFMutableSetRef _sources1; // <set>
    CFMutableArrayRef _observers; // <Array>
    CFMutableArrayRef _times; // <Array>
    // ...
};

// CFRunloop.h 类型重命名
typedef struct __CFRunLoop *CFRunLoopRef;
// CFRunloop.c 结构体
struct __CFRunLoop {
	// ...
    pthread_t _pthread; // runloop 执行线程(runloop 和线程的关系是一一对应)
    // ...
    CFMutableSetRef _commonModes; // <set> String UITrackingRunloopMode / KCFRunloopDefaultMode (一个存储了被标记为 common modes 的模式集合)
    CFMutableSetRef _commonModeItems; // <set> Timer / Observer / Source
    CFRunLoopModeRef _currentMode; // 当前运行的 mode
    CFMutableSetRef _modes; // 内置的 modes
    // ...
};

Runloop 内部存在一个 modes 集合,但 Runloop 只能运行一个 Mode, Runloop 只会处理它当前 Mode 的事件。
请添加图片描述
Runloop 运行模式

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

Runloop 中的 commonModes 是一个集合,里面存储着被标记 kCFRunloopCommonModes 的 mode 的 name。

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

Source1:基于mach_port,来自系统内核或者其他进程或线程的事件,可以主动唤醒休眠中的 Runloop。

Observer – CFRunloopObserver

  • KCFRunloopEntry (runloop 准备启动)
  • KCFRunloopBeforeTimers (通知观察者,runloop 将要对 Timer 的一些相关事件进行处理了)
  • KCFRunloopBeforeSoureces(将要处理一些 Sources 事件)
  • KCFRunloopBeforeWaiting(即将要发生用户态到内核态的切换)
  • KCFRunloopAfterWaiting(内核态转换为用户态)
  • KCFRunloopExit(runloop 退出通知)

Timer – CFRunloopTimer:是基于时间的触发器,当加入到 Runloop 时,Runloop 会注册对应的时间点,当时间点到时,Runloop 会被唤醒以执行回调。

Runloop 中的 commonModeItems 是当前运行在 commonModes 模式下的 CFRunloopSource / CFRunloopObserver / CFRunloopTimer,其实就是 kCFRunLoopDefaultMode 和 UITrackingRunLoopMode 的 source1 / timter / observers 都包含在 commonModeItems 里面。

当 commonModes 里每多一个 mode, commonModeItems 里就会多一组 source1 / timter / observers

三、Runloop 执行原理

在这里插入图片描述

Runloop 伪代码

SetupThisRunLoopRunTimeoutTimer(); // by GCD timer

//通知即将进入runloop__CFRUNLLOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(KCFRunLoopEntry);
do {

     __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);

     __CFRunLoopDoObservers(kCFRunLoopBeforeSources);

     __CFRunLoopDoBlocks();  //一个循环中会调用两次,确保非延迟的NSObject PerformSelector调用和非延迟的dispatch_after调用在当前runloop执行。还有回调block

     __CFRunLoopDoSource0(); //例如UIKit处理的UIEvent事件

     CheckIfExistMessagesInMainDispatchQueue(); //GCD dispatch main queue

     __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting); //即将进入休眠,会重绘一次界面

     // 休眠,等待事件唤醒
     var wakeUpPort = SleepAndWaitForWakingUpPorts();

     // mach_msg_trap,陷入内核等待匹配的内核mach_msg事件

     // Zzz...

     // Received mach_msg, wake up

     __CFRunLoopDoObservers(kCFRunLoopAfterWaiting);

     // Handle msgs

     if (wakeUpPort == timerPort) {

          __CFRunLoopDoTimers();

     } else if (wakeUpPort == mainDispatchQueuePort) {

          //GCD当调用dispatch_async(dispatch_get_main_queue(),block)时,libDispatch会向主线程的runloop发送mach_msg消息唤醒runloop,并在这里执行。这里仅限于执行dispatch到主线程的任务,dispatch到其他线程的仍然是libDispatch来处理。

          __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()

     } else {

          __CFRunLoopDoSource1();  //CADisplayLink是source1的mach_msg触发?

     }

     __CFRunLoopDoBlocks();

} while (!stop && !timeout);

//通知observers,即将退出runloop
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBERVER_CALLBACK_FUNCTION__(CFRunLoopExit);

3.1 runloop 处理的 6 类事件

1、timer (延迟的 PerformSelector 方法调用,延迟的 dispatch_after,timer事件)
2、observer(runloop中状态变化时进行通知(UI 观察者等))
a. 卡顿检测(1、开辟新线程定时去获取值的状态;2、observer 监听 runloop 的 beforeTimer 开始, beforeWaitingSleep / exit 结束)
3、source0(app 内部事件处理:UIEvent 时间,CFSocket 事件,触摸事件其实是Source1接收系统事件后在回调 __IOHIDEventSystemClientQueueCallback() 内触发的 Source0,Source0 再触发的 _UIApplicationHandleEventQueue()。source0一定是要唤醒runloop及时响应并执行的,如果runloop此时在休眠等待系统的 mach_msg事件,那么就会通过source1来唤醒runloop执行。)
4、source1(处理系统内核的mach_msg事件)
5、Block(PerformSelector 非延迟方法、dispatch_after 立刻调用,block 调用)
6、Main Queue(GCD 中 dispatch 到 main queue 的 block 会被 dispatch 到 main loop执行)

3.2 Runloop mode 切换

上节我们知道,Runloop 每次只能在当前 mode 下执行,那如果需要在不同的 mode 下切换系统是怎么处理的呢?比如界面从静止到滑动时,mode 是如何由 NSRunloopDefaultMode 切换到 UITrackingRunloopMode呢。

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

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

四、Runloop 实际应用

4.1 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 了。

4.2 事件响应

苹果注册了一个 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 事件都是在这个回调中完成的。

4.3 手势识别

当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。
苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。
当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

4.4 界面更新

runloop 在 APP 启动的时候会注册一个 UI Observer, UI 变动(frame 变动、UIView/CALayer 层级的变动、 手动设置了 setNeedsLayout/setNeedsDisplay 等)的操作将会提交到全局容器,当 UI Observer 监听到主线程 runloop 进入休眠或者退出的状态,回调去执行一个很长的函数:_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

4.5 按钮点击

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/462894.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

电视盒子什么牌子好?数码博主盘点2022电视盒子排行榜

网络电视盒子是电视机的标配&#xff0c;开放性的安卓系统能观看海量视频资源&#xff0c;我每年也会进行电视盒子的测评&#xff0c;今天要来分享五款最热门的网络电视盒子推荐&#xff0c;跟着我一起看看网络电视盒子哪个好。 一&#xff1a;泰捷WEBOX60Pro电视盒子 年度…

【华为HCIP | 高级网络工程师】刷题日记(1)

个人名片&#xff1a; &#x1f43c;作者简介&#xff1a;一名大二在校生&#xff0c;讨厌编程&#x1f38b; &#x1f43b;‍❄️个人主页&#x1f947;&#xff1a;落798. &#x1f43c;个人WeChat&#xff1a;落. &#x1f54a;️系列专栏&#xff1a;&#x1f5bc;️ 零基础…

03-stable diffusion国风小姐姐

stable diffusion 文生图 – 生成国风小姐姐 一、模型在哪里下载 下载网站civitai&#xff1a; Civitai | Stable Diffusion models, embeddings, LoRAs and more国风主模型&#xff1a;https://civitai.com/models/14171/cutegirlmix4主模型放到sd-webui-aki-v4\models\Stab…

【AUTOSAR】【信息安全】CSM

目录 一、概述 二、依赖模块 三、功能描述 3.1 基本体系结构 3.2 通用行为 3.2.1 正常操作 3.2.2 设计说明 3.3 错误分类 3.3.1 开发错误 3.3.2 运行时错误 四、API接口 4.1 通用接口 4.2 加密接口 4.3 秘钥接口 4.4 Job接口 4.5 回调接口 4.6 调度接口 一、概…

配置中心基本原理

配置中心是如何实现推送的&#xff1f; 背景 传统的静态配置方式想要修改某个配置时&#xff0c;必须重新启动一次应用&#xff0c;如果是数据库连接串的变更&#xff0c;那可能还容易接受一些&#xff0c;但如果变更的是一些运行时实时感知的配置&#xff0c;如某个功能项的…

ROS学习第四十一节——SLAM建图

https://download.csdn.net/download/qq_45685327/87721374 准备工作 请先安装相关的ROS功能包: 安装 gmapping 包(用于构建地图):sudo apt install ros-melodic-gmapping 安装地图服务包(用于保存与读取地图):sudo apt install ros-melodic-map-server 安装 navigation 包…

Java -- 多线程

多线程 并发 在同一时刻&#xff0c;有多个指令在单个CPU上交替执行 CPU在多个线程之间交替执行 并行 在同一时刻&#xff0c;有多个指令在多个CPU上同时执行 多线程的实现方式 继承Thread类的方法进行实现实现Runnable接口的方式进行实现利用Callable接口和Future接口方…

数据库基础篇 《17.触发器》

数据库基础篇 《17.触发器》 在实际开发中&#xff0c;我们经常会遇到这样的情况&#xff1a;有 2 个或者多个相互关联的表&#xff0c;如商品信息和库存信息分别存放在 2 个不同的数据表中&#xff0c;我们在添加一条新商品记录的时候&#xff0c;为了保证数据的完整性&#…

【刷题之路】LeetCode 203. 移除链表元素

【刷题之路】LeetCode 203. 移除链表元素 一、题目描述二、解题1、方法1——在原链表上动刀子1.1、思路分析1.2、代码实现 2、方法2——使用额外的链表2.1、思路分析2.2、代码实现 一、题目描述 原题连接&#xff1a; 203. 移除链表元素 题目描述&#xff1a; 给你一个链表的…

跨数据中心下的 Kafka 高可用架构分析

导语 本文介绍了 Kafka 跨数据中心的两种部署方式&#xff0c;简要分析两种方式下的不同架构以及优缺点&#xff0c;对这些架构可能碰到的问题也提供了一些解决思路&#xff1b;同时也说明了 Kafka 跨数据中心部署的社区解决方案和商业化解决方案。 背景 Kafka 作为世界上最…

laravel5.6.* + vue2 创建后台

本地已经安装好了composer 1.新建 Laravel5.6.*项目 composer create-project --prefer-dist laravel/laravel laravel5vue2demo 5.6.* 2. cd laravel5vue2demo 3. npm install /routes/web.php 路由文件中, 修改 Route::get(/, function () {return view(index); });新建…

第三方jar包引入项目,发布到本地和远程仓库

在开发过程中&#xff0c;往往会遇到对接其他公司的系统。然后对接公司会提供API对接方式&#xff0c;就是给一个jar包。我们只需要把jar包引入到项目中直接用即可。本地引用jar的话可以有两种方式。第一种就是本地包引用&#xff0c;如下将包放下工程下&#xff0c;然后maven指…

【五一劳动节来了】

今年“五一”&#xff0c;4月29日至5月3日放假调休&#xff0c;共5天。 如果你在5月4日到5月6日请假3天&#xff0c;加上5月7日周日&#xff0c;就可以形成9天的假期。 一&#xff0c;五一劳动节的由来⭐ 国际劳动节又称“五一国际劳动节”“国际示威游行日”&#xff08;英语…

抢先看,甘特图工具DHTMLX gantt 灯箱编辑器通过套件 UI 小部件进行了扩展

DHTMLX Gantt是用于跨浏览器和跨平台应用程序的功能齐全的Gantt图表。可满足项目管理应用程序的大部分开发需求&#xff0c;具备完善的甘特图图表库&#xff0c;功能强大&#xff0c;价格便宜&#xff0c;提供丰富而灵活的JavaScript API接口&#xff0c;与各种服务器端技术&am…

【STL十四】函数对象(function object)_仿函数(functor)——lambda表达式

【STL十四】函数对象&#xff08;function object&#xff09;_仿函数&#xff08;functor&#xff09;——lambda表达式 一、函数对象&#xff08;function object&#xff09;二、函数对象优点三、分类四、头文件五、用户定义函数对象demo六、std::内建函数对象1、 算术运算函…

YARN 远程代码执行(RCE)安全漏洞问题分析与解决方案

YARN 远程代码执行&#xff08;RCE&#xff09;安全漏洞问题分析与解决方案 1 YARN RCE 漏洞问题问题现象 某客户使用Tenable.sc扫描安全漏洞后反馈&#xff0c;YARN 存在Remote code execution (RCE) 安全漏洞问题&#xff0c;攻击者可在未经过身份验证的情况下通过该漏洞在…

【21】核心易中期刊推荐——人工智能 | 遥感图像识别

🚀🚀🚀NEW!!!核心易中期刊推荐栏目来啦 ~ 📚🍀 核心期刊在国内的应用范围非常广,核心期刊发表论文是国内很多作者晋升的硬性要求,并且在国内属于顶尖论文发表,具有很高的学术价值。在中文核心目录体系中,权威代表有CSSCI、CSCD和北大核心。其中,中文期刊的数…

psql在建表时,分为常规、外部、分区,三者有什么区别?如何从建表语句中区分?

在 PostgreSQL 中&#xff0c;常规表、外部表和分区表都可以通过 CREATE TABLE 语句进行创建&#xff0c;它们的创建语法略有不同&#xff0c;通过创建语句可以很明显地区分它们的类型。 以下是常规表、外部表和分区表的创建语法及示例&#xff1a; 1. 常规表 常规表是最常见…

Spring核心与设计思想、创建与使用

文章目录 一、Spring是什么二、为什么要学习框架三、IoC和DI&#xff08;一&#xff09;IoC1. 认识IoC2. Spring的核心功能 &#xff08;二&#xff09;DI 四、Spring项目的创建&#xff08;一&#xff09;使用 Maven 方式创建一个 Spring 项目 五、Spring项目的使用&#xff0…

少年与阿童木:一场软件竞技赛背后的智能未来

1961年&#xff0c;手冢治虫创办了虫制作株式会社&#xff0c;带领团队开始尝试将此前的漫画作品进行动画化。1963年的元旦&#xff0c;他们的首部作品一经播出就引发轰动&#xff0c;这部动画的名字叫做——《铁臂阿童木》。 一晃数十年&#xff0c;阿童木已经成为了几代人对A…