Runloop解析

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)]

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

前端review

关于实时预览vs code中的颜色代码需要安装的插件,包括html文件格式中的颜色代码安装Flutter Color插件 VSCode 前端常用插件集合 1.Auto Close Tag自动闭合HTML/XML标签 2.Auto Rename Tag自动完成另一侧标签的同步修改 3.Beautify格式化代码,值得注…

Python武器库开发-前端篇之html概述(二十八)

前端篇之html概述(二十八) html概述 HTML5是构建Web内容的一种语言描述方式。HTML5是互联网的下一代标准,是构建以及呈现互联网内容的一种语言方式.被认为是互联网的核心技术之一。HTML产生于1990年,1997年HTML4成为互联网标准,…

斐讯K2结合Padavan实现锐捷认证破解方法

前言 众所周知,校园网在传统模式下是不能直接插路由使用的,但苦于校园网只能连接一台设备的烦恼,不得不“另辟蹊径”来寻求新的解决路径,这不,它来了,它来了,它带着希望走来了。 本文基于斐讯…

基于Haclon的图形镜像案例

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

Android平台GB28181设备接入模块开发填坑指南

技术背景 为什么要开发Android平台GB28181设备接入模块?这个问题不再赘述,在做Android平台GB28181客户端的时候,媒体数据这块,我们已经有了很好的积累,因为在此之前,我们就开发了非常成熟的RTMP推送、轻量…

Deepin使用记录-deepin系统下安装RabbitMq

目录 0、引言 1、由于RabbitMq是erlang语言开发的,所有需要先安装erlang 2、更新源并安装RabbitMq 3、安装完成之后,服务是启动的,可以通过以下语句查看状态 4、这样安装完成之后,是看不到web页面的,需要再安装一…

PTA NeuDs_数据库题目

二.单选题 1.数据库应用程序的编写是基于数据库三级模式中的。 A.模式 B.外模式 C.内模式 D.逻辑模式 用户应用程序根据外模式进行数据操作,通过外模式一模式映射,定义和建立某个外模式与模式间的对应关系 2.对创建数据库模式一类的数据库对象的授权…

【开源项目】C#.NET 扩展库 -- Com.Gitusme.Net.Extensiones.Core

目录 1、项目介绍 2、集成方式 方法一:项目中通过Nuget包管理器安装导入 方法二:手动从Nuget官网下载,下载地址: 3、代码中导入命名空间 4、版本变更说明 1.0.7 版本 1.0.6 版本 1.0.5 版本 1.0.4 版本 5、演示示例 示…

Unity SRP 管线【第三讲:URP 光照】

3.2.3 以前属于Shader部分,Shader部分不进行讲解。 这里只涉及Unity内部管线的设置问题。 文章目录 3.2.3 向GPU发送灯光数据设置光源数据设置主光源设置额外点光源 Shader中的数据 3.2.3 向GPU发送灯光数据 在UniversalRenderPipeline.cs > RenderSingleCamera…

叠加原理(superposition principle)

叠加原理(superposition principle)指对线性系统而言,两个或多个输入产生的输出,等于这几个输入单独引起的输出的和,即输入的叠加等于各输入单独引起的输出的叠加。 例如,如果输入产生的输出是,…

如何使用nginx部署静态资源

Nginx可以作为静态web服务器来部署静态资源,这个静态资源是指在服务端真实存在,并且能够直接展示的一些文件数据,比如常见的静态资源有html页面、css文件、js文件、图片、视频、音频等资源相对于Tomcat服务器来说,Nginx处理静态资…

大数据面试大厂真题【附答案详细解析】

1.Java基础篇(阿里、蚂蚁、字节、携程、快手、杭州银行等) 问题:HashMap的底层实现原理 答案: 在jdk1.8之前,hashmap由 数组-链表数据结构组成,在jdk1.8之后hashmap由 数组-链表-红黑树数据结构组成&…

②⑩② 【读写分离】Sharding - JDBC 实现 MySQL读写分离[SpringBoot框架]

个人简介:Java领域新星创作者;阿里云技术博主、星级博主、专家博主;正在Java学习的路上摸爬滚打,记录学习的过程~ 个人主页:.29.的博客 学习社区:进去逛一逛~ Sharding-JDBC Sharding-JDBC介绍使用 Shardin…

DistilBERT模型训练实战

LLM似乎正在接管世界,但许多人仍然不真正理解他们是如何运作的。 我从事机器学习工作已有几年,并且对自然语言处理和最近的进展非常着迷。 尽管我阅读了大部分随附的论文,但训练这些模型对我来说仍然是一个谜,这就是为什么我决定…

Java(七)(Lambda表达式,正则表达式,集合(Collection,Collection的遍历方式))

目录 Lambda表达式 省略写法(要看懂) 正则表达式 语法 案例 正则表达式的搜索替换和分割内容 集合进阶 集合体系结构 Collection Collection的遍历方式 迭代器 增强for循环 Lambda表达式遍历Collection List集合 ArrayList LinkedList 哈希值 HashSet底层原理 …

中东客户亲临广东育菁装备参观桌面型数控机床生产

近日,中东地区的一位重要客户在广东育菁装备有限公司的热情接待下,深入了解了该公司生产的桌面型数控机床。这次会面不仅加强了双方在业务领域的交流,也为中国与中东地区的经济合作描绘出更美好的前景。 在育菁装备公司各部门主要负责人及工作…

赋值,浅拷贝,深拷贝

1.前置知识 数据分为基本类型(String, Number, Boolean, Null, Undefined,Symbol)和引用类型(Object)基本类型:直接存储在栈内存中的数据引用类型:指向改数据的指针变量存储在栈内存中,真实的数据存储在堆内存中引用类型在栈内存…

cephadm部署ceph quincy版本,使用ceph-csi连接

环境说明 IP主机名角色 存储设备 192.168.2.100 master100 mon,mgr,osd,mds,rgw 大于5G的空设备192.168.2.101node101mon,mgr,osd,mds,rgw大于5G的空设备192.168.2.102node102mon,mgr,osd,mds,rgw大于5G的空设备 关闭防火墙 关闭并且禁用selinux 配置主机名/etc/hosts …

【UGUI】中Content Size Fitter)组件-使 UI 元素适应其内容的大小

官方文档:使 UI 元素适应其内容的大小 - Unity 手册 必备组件:Content Size Fitter 通常,在使用矩形变换定位 UI 元素时,应手动指定其位置和大小(可选择性地包括使用父矩形变换进行拉伸的行为)。 但是&a…