Runloop解析

news2024/11/28 2:40:16

RunLoop

前言

​ 本文介绍RunLoop的概念,并使用swift和Objective-C来描述RunLoop机制。

简介

​ RunLoop——运行循环(死循环),它提供了一个事件循环机制在程序运行过程中处理各种事件,例如用户交互、网络请求、定时器等等。 RunLoop可以在需要的时候自己跑起来运行,在没有操作的时候就停下来休息,充分节省CPU资源,提高程序性能。

基本思想

循环的处理事件。RunLoop在主线程运行,负责管理该线程中的事件,并确保UI更新等重要任务能够顺利执行,RunLoop启动时,后进入无限循环,等待事件发生。当有事件发生时,RunLoop会调用相应的处理方法来处理该事件,并继续等待下一个事件发生。RunLoop会一直运行,直到被手动停止或应用程序退出。

目的

——保证RunLoop所在线程不退出

——负责监听事件。iOS触摸、时钟、网络。

RunLoop与线程

​ 在iOS中,每个线程都有一个RunLoop(一一对应),但默认情况下,只有主线程RunLoop是开启的,其他线程都是禁用的。要使用RunLoop,必须手动启动它,并将其添加到线程的运行循环中。

RunLoop对象

Foundation框架 (基于CFRunLoopRef的封装) NSRunLoop对象

CoreFoundation CFRunLoopRef对象

NSRunLoop是基于CFRunLoopRef的一层OC封装

RunLoop运行模式

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

RunLoop优先处理UI模式的事件,而UI模式只能被UI事件唤醒

  1. NSDefaultRunLoopMode 默认模式 —— 一般处理timer\网络事件
  2. UITrackingRunLoopMode UI模式 —— 专门处理UI事件
  3. NSRunLoopCommonModes 占位模式( UI && 默认)
  4. UIInitializationRunLoopMode 在刚启动App时第进入的第一个Mode,启动完成后就不再使用
  5. GSEventReceiveRunLoopMode 接受系统事件的内部Mode

Timer:定时器

iOS开发中,定时器是一种常见的事件,例如每隔一段时间刷新UI、执行后台任务等等。RunLoop提供了定时器(timer)机制,用于在指定时间间隔内执行某个操作。

var timer1: Timer?
    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = .white
        testMode()
    }
    
    func testMode(){
        let scrollView = UIScrollView(frame: CGRect(x: 50.0, y: 100.0, width: 100.0, height: 100.0))
        scrollView.backgroundColor = .orange
        scrollView.contentSize = CGSize(width: 100.0, height: 200.0)
        self.view.addSubview(scrollView)
        timer1 = Timer(timeInterval: 2.0, target: self, selector: #selector(runLoopAction), userInfo: nil, repeats: true)
        RunLoop.current.add(timer1!, forMode: .common)
        
    }
    @objc func runLoopAction(){
        NSLog("=== run ===")
    }

运行结果:

使用 kCFRunLoopDefaultMode 模式时,滑动 UIScrollView 不打印.
使用 UITrackingRunLoopMode 模式时,只有滑动 UIScrollView 才会打印. 
使用 kCFRunLoopCommonModes 模式时,不管滑不滑动 UIScrollView 都会打印

**Source:**事件源

按照函数调用栈

  • Source0:非Source1 用于用户主动触发的事件(点击button 或点击屏幕)(数据结构:[machport:value])machport理解成进程间相互发送消息的一种机制。
  • Source1:基于port的系统内核事件,主动唤醒runloop(数据结构:数组)

简单举个例子:一个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卡顿的功能(例如渲染图片,从waiting之前一次一次渲染)。

Perform Selector

Perform Selector是一种调用方法的方式,可以在RunLoop中异步执行某个方法。在调用方法时,可以设置延迟执行时间和RunLoop模式。该方法会在指定的时间间隔内执行,直到被取消。

例如,要在主线程中使用Perform Selector,可以使用如下代码:

RunLoop.current.perform(#selector(doSomething), target: self, argument: nil, order: 0, modes: [.default])

这将在默认模式下异步执行doSomething方法。

事件循环的时间机制

截屏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)

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

RunLoop的常用操作

除了上述基本操作之外,RunLoop还提供了其他常用操作,例如:

  1. stop:停止RunLoop的运行。
  2. runUntilDate:运行RunLoop直到指定日期。
  3. runMode:运行RunLoop指定模式下的事件处理循环。
  4. currentMode:获取当前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() 进行应用内部的分发,触发Source0。

_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 内的。

RunLoop与线程安全

iOS开发中,多线程是一个常见的问题。RunLoop在处理异步事件时,可能会导致线程不安全的问题。为了保证RunLoop的线程安全,可以使用以下方法:

  1. 使用RunLoopQueue,在队列中使用RunLoop来执行异步操作。
  2. 在主线程中使用RunLoop来处理异步事件,避免跨线程操作

q1: RunLoop可以做什么?

  1. 处理Crash(程序崩溃不退出)
  2. 保持线程存活(线程保活)
  3. 监测和优化App的卡顿

线程保活(NSOperation和GCD一样可以)(NSCondition加锁保活,不涉及RunLoop)

​ 如果项目需求比较复杂,很多操作都需要在子线程进行,比如有很多耗时操作(图片绘制,视频下载等等),子线程执行完任务之后会自动销毁,频繁的线程创建和销毁会导致资源浪费,此时就可以使用RunLoop进行线程保活而不被销毁。我们知道,当子线程中的任务执行完毕之后就被销毁了,那么如果我们需要开启一个子线程,在程序运行过程中永远都存在,那么我们就会面临一个问题,如何让子线程永远活着,这时就要用到常驻线程:给子线程开启一个RunLoop 注意:子线程执行完操作之后就会立即释放,即使我们使用强引用子线程使子线程不被释放,也不能给子线程再次添加操作,或者再次开启。 子线程开启RunLoop的代码,先点击屏幕开启子线程并开启子线程RunLoop,然后点击button。

q2:线程和RunLoop什么关系?

**RunLoop存储方式:**键值对(线程 :runloop)

所以runloop和线程是一一对应的。

q3:RunLoop组成

Mode->sources/timer/observer(卡顿检测)

如果没有sources或timer直接进入休眠状态

**CFRunLoop和NSRunLoop区别:**CFRunLoop是在CoreFoundation中用纯c语言实现的,它提供一个c函数API,是线程安全的;而NSRunLoop是基于CF的封装,提供的是面向对象的API,非线程安全。

q4:RunLoop怎么启动

  1. run
  2. run(until)
  3. run(mode,until)

使用第三种,自己构造runloop循环,并且线程不能设置为强引用(或者自己设置为nil)

卡顿监测优化

卡顿跟硬件有关CPU、GPU

影响CPU性能:IO任务,过多的线程抢占CPU资源、温度过高降频

影响GPU性能:显存频率、渲染算法、大计算量

UIKit不是一个线程安全的框架,所以UI操作等都需要在主线程操作,故复杂任务一般放子线程执行,这也是线程保活意义所在。

**如何监测卡顿:**fps,59.94/s,ping,runloop

通过 CFRunLoopObserverRef来监测

处理时机:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

U

影响CPU性能:IO任务,过多的线程抢占CPU资源、温度过高降频

影响GPU性能:显存频率、渲染算法、大计算量

UIKit不是一个线程安全的框架,所以UI操作等都需要在主线程操作,故复杂任务一般放子线程执行,这也是线程保活意义所在。

**如何监测卡顿:**fps,59.94/s,ping,runloop

通过 CFRunLoopObserverRef来监测

处理时机:

[外链图片转存中…(img-kZRaHKGR-1700973808112)]

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

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

相关文章

ps5ps4游戏室如何计时?计费系统怎么查看游戏时间以及收费如何管理

ps5ps4游戏室如何计时?计费系统怎么查看游戏时间以及收费如何管理 1、ps5ps4游戏室如何计时? 下图以佳易王计时计费软件V17.9为例说明 在开始计时的时候,只需点 开始计时按钮,那么开台时间和使用的时间长度项目显示在屏幕上&am…

Pure-Pursuit 跟踪五次多项式轨迹

Pure-Pursuit 跟踪五次多项式轨迹 考虑双移线轨迹 X 轴方向位移较大,机械楼停车场长度无法满足 100 ~ 120 m,因此采用五次多项式进行轨迹规划,在轨迹跟踪部分也能水一些内容 调整 double_lane.cpp 为 ref_lane.cpp,结合 FrenetP…

基于 GPS 定位信息的 Pure-Pursuit 轨迹跟踪实车测试(1)

基于 GPS 定位信息的 Pure-Pursuit 轨迹跟踪实车测试(1) 进行了多组实验,包括顺逆时针转向,直线圆弧轨迹行驶,以及Pure-Pursuit 轨迹跟踪测试 代码修改 需要修改的代码并不多,主要对 gps_sensor 功能包和…

蓝桥杯每日一题2023.11.26

题目描述 奖券数目 - 蓝桥云课 (lanqiao.cn) 将每一个数字进行一一枚举&#xff0c;如果检查时不带有数字4则答案可以加1 #include<bits/stdc.h> using namespace std; int ans; bool check(int n) {while(n){if(n % 10 4)return false;n / 10; }return true; } int m…

基于Haclon的标签旋转项目案例

项目要求&#xff1a; 图为HALCON附图“25interleaved_exposure_04”&#xff0c;里面为旋转的二维码标签&#xff0c;请将其旋转到水平位置。 项目知识&#xff1a; 在HALCON中进行图像平移和旋转通常有以下步骤&#xff1a; &#xff08;1&#xff09;通过hom_mat2d_ident…

<JavaEE> Thread线程类 和 Thread的常用方法

目录 一、Thread概述 二、构造方法 三、常用方法 1.1 getId()、getName()、getState()、getPririty() 1.2 start() 1.3 isDaemon()、setDaemon() 1.4 isAlive() 1.5 currentThread() 1.6 Interrupt()、interrupted()、isInterrupted() 1.6.1 方法一&#xff1a;添加共…

基于Haclon的图形镜像案例

项目要求&#xff1a; 图为HALCON的例图“green-dot”&#xff0c;请将其中的圆形图案按水平和垂直两个方向分别进行镜像。 项目知识&#xff1a; 首先要用BLOB分析的方法&#xff0c;得到圆形图案的目标区域&#xff0c;再对其进行镜像。 在HALCON中与镜像相关的算子为mirr…

跟着chatgpt学习|1.spark入门

首先先让chatgpt帮我规划学习路径&#xff0c;使用Markdown格式返回&#xff0c;并转成思维导图的形式 目录 目录 1. 了解spark 1.1 Spark的概念 1.2 Spark的架构 1.3 Spark的基本功能 2.spark中的数据抽象和操作方式 2.1.RDD&#xff08;弹性分布式数据集&#xff09; 2…

实战oj题——括号匹配问题

前言&#xff1a;前面我们已经做了一些关于顺序表和链表的oj题&#xff0c;今天我们就来解决一些有关于栈和队列的oj题。 我们对这个题看起来毫无头绪&#xff0c;但是我们刚学习了栈&#xff0c;就可以用栈来解决这一类问题&#xff0c;如果我们读取到左括号就入栈&#xff0c…

Cache学习(3):Cache地址映射(直接映射缓存组相连缓存全相连缓存)

1 Cache的与存储地址的映射 以一个Cache Size 为 128 Bytes 并且Cache Line是 16 Bytes的Cache为例。首先把这个Cache想象成一个数组&#xff0c;数组总共8个元素&#xff0c;每个元素大小是 16 Bytes&#xff0c;如下图&#xff1a; 现在考虑一个问题&#xff0c;CPU从0x0654…

再见 Pandas,再见算法

大家好,《再见pandas》 系列已有200多位朋友加入学习了,这段时间亲眼见证了很多朋友的飞跃进步,从无到有,从一个问问题的小白到开始慢慢回答别人的问题,在讨论和练习中不断成长。虽说pandas已经很普及了,但普及内容的深度却远远不够。 下面这套原创图文是我和几位小伙伴…

深入了解 Pinia:现代 Vue 应用的状态管理利器

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

手把手教你如何在Linux下写进度条小程序(附源码)

效果展示 录屏2023 一、建立文件 mkdir ProgressBar //在当前目录下&#xff0c;建立新的目录 cd ProgressBar //进入这个目录 touch main.c makefile progressbar.c progressbar.h //在ProgressBar这个目录建立这几个文件进入ProgressBar这个目录之后&#xff0c;使…

python树长子兄弟链存储结构(孩子兄弟链存储结构)

长子兄弟链存储结构&#xff08;孩子兄弟链存储结构&#xff09;解释&#xff1a; 长子兄弟链存储结构是一种树的存储结构&#xff0c;它使用孩子兄弟表示法&#xff08;也称作左孩子右兄弟表示法&#xff09;来表示树的结构。这种表示方法主要用于存储一般的树&#xff0c;而不…

Linux服务器SSH客户端断开后保持程序继续运行的方法

目录 1. nohup 命令&#xff1a; 2. tmux 或 screen&#xff1a; 3 final shell 断开后服务器如何继续执行令&#xff1f; 方法一&#xff1a;使用 nohup 命令 方法二&#xff1a;将命令放在后台执行 4 你可以使用 jobs 命令查看当前终端中正在后台运行的任务 &#xff…

飞翔的鸟小游戏

第一步是创建项目 项目名自拟 第二步创建个包名 来规范class 再创建一个包 来存储照片 如下&#xff1a; package game; import java.awt.*; import javax.swing.*; import javax.imageio.ImageIO;public class Bird {Image image;int x,y;int width,height;int size;doub…

Qt4用子类化ProxyModel和子类化MainWindow实现全表筛选,中文排序和复制粘贴

目录 1 需求 2 子类化ProxyModel实现全表筛选 3 字符串列表实现中文排序 3.1 Qt5中文排序 3.2 Qt4排序 4 表格的复制粘贴 5 应用 1 需求 模型视图编程是Qt开发的基本功&#xff0c;其中有几个关键问题需要解决&#xff1a; 全表筛选&#xff0c;或者说多列搜索中文排序…

【精选必看】MyBatis映射文件及动态SQL,一级,二级缓存介绍

文章目录 MyBatis映射文件 < r e s u l t M a p > <resultMap> <resultMap>resultMap < sql>&< include>特殊字符处理 动态SQL < i f > < if> <if> < w h e r e > <where> <where> < s e t > <…

叠加原理(superposition principle)、线性系统

叠加原理&#xff08;superposition principle&#xff09;&#xff1a;指对一个系统而言&#xff0c;两个或多个输入产生的输出&#xff0c;等于这几个输入单独引起的输出的和&#xff0c;即输入的叠加等于各输入单独引起的输出的叠加。 线性系统&#xff1a;一个系统&#x…

SAP Smartform小结

SAP系统做打印单据用的, 感觉很不好用, 特别是要嵌入韩文时必须使用嵌入的word编辑器,运行速度简直不可忍受. 见过一些Adobe interactive form的示例, 看着相当不错, 不过据说需要花money额外买licence, 哪有smartform这种免费东西来得实惠. 一般打印需求,会要求有标题抬头,打…