当前位置: 首页 > news >正文

Runloop

假设你的项目中有关tableView,然后还有一个定时器timer在执行,定时器代码如下:

    var num = 0override func viewDidLoad() {super.viewDidLoad()let timer = Timer(timeInterval: 1,target: self,selector: #selector(self.run),userInfo: nil,repeats: true)RunLoop.current.add(timer, forMode: .default)}@objc func run() {num += 1print(Thread.current ,num)Thread.sleep(forTimeInterval: 3)}

当你滚动tableView时,你会发现timer定时器会停止,当停止滚动tableView,timer定时器会继续进行。这是什么原因呢?

如果学过线程,你会知道:主线程的优先级是最高的,主线程也叫UI线程,也就是说UI事件必须在主线程中进行,因此在iOS中,UI事件会优先进行。

什么是Runloop?

Runloop是一种事件循环机制,用来循环的处理线程中的事件,当Runloop启动时,如果线程中有事件,它就会调用方法来处理该事件,如果没有事件,就会进入休眠,等该线程中有事件需要处理时才会被唤醒。

线程和Runloop的关系

线程和Runloop是一一对应的,它们的关系保存在一个全局的Dictionary中,其中线程是Key,Runloop是Value,对于主线程中的Runloop是在程序启动时自动创建并启动,而其它子线程的Runloop是懒加载的,也就是说,只有子线程有任务需要被执行时Runloop才会创建并启动。

Runloop的功能

Runloop的功能:

  • 线程保活,Runloop会在线程中没有任务时让线程进入休眠而不会退出,比如保活主线程,使得应用程序不会退出。
  • 负责监听事件,如网络事件,计时器事件,触摸事件
  • 渲染UI,一次Runloop循环,需要渲染屏幕上所有变化的像素点
  • 节省CPU开销,让程序该休息时休息

Runloop的基本组成

输入源

输入源(Input Source)用于处理异步事件,比如用户交互事件,网络事件等,Runloop运行时,会检查当前线程是否有输入源需要处理,如果有会立即处理。

输入源也可以分为两种source:source0和source1,其中source1基于port的系统内核事件,可以主动唤醒runloop,在底层是通过Dictionary的Value来存储的,其中Key是machport。而source0是不基于port的,是应用程序主动触发的事件(如:按钮点击触发事件),不会主动唤醒runloop。

如:当我们点击按钮时,触发的事件会被包装成Event,Event通过machport机制告诉source1,source1主动唤醒runloop,然后将事件Event分发给Source0,然后由Source0来处理。

定时器源

定时执行的任务,如NSTimer,Runloop会定期检测定时器,到达时间执行相应操作。

观察者

用于监听Runloop的状态,基本状态如下:

kCFRunloopEntry (runloop准备启动)
kCFRunloopBeforeTimers (通知观察者,runloop将要对Timer的一些相关事件进行处理了)
kCFRunloopBeforeSources (将要处理一些Sources事件)
kCFRunloopBeforeWaiting( 即将要发生用户态到内核态的切换 用户态 —> 内核态)没事做进入内核态避免资源浪费,即:即将进入休眠。
kCFRunloopAfterWaiting (内核态—转—>用户态)即将被唤醒
kCFRunloopExit (runloop退出通知)

Runloop的几种模式

Runloop主要分为以下几种模式:

NSDefaultRunloopMode:默认模式,处理大多数应用事件,比如Timer定时器事件,网络事件等。

NSRunloopCommonModes:常用模式,允许 RunLoop 同时处理多种事件类型。使用场景包括当用户滚动 UIScrollView 时仍能处理定时器事件。

UITrackingRunloopMode:界面追踪模式,处理UI事件,比如滚动tableView时,保证不受其它Mode影响。

UIInitializationRunLoopMode :在刚启动App时第进入的第一个Mode,启动完成后就不再使用

GSEventReceiveRunLoopMode :接受系统事件的内部Mode

这里可以发现,上面我们的timer添加在default模式下,而tableView的滚动是在Tracking模式下的,所以当tableView滚动时不受其它Mode影响,所以timer定时器事件停止执行,而停止滚动tableView,Runloop切换到default模式,正常处理Timer事件。

如果timer添加在common模式下,timer事件不会受tabelView滚动影响,因为tableView滚动时,在Tracking模式下,可以让标记为"common"的事件继续进行。

如果timer添加在Tracking模式下,当滚动tableView时,timer方法执行,当tableView滚动停止,方法不执行。

阻塞

在上面的讲解中,当定时器设置成common模式,UI事件(tableView滚动)进行时,timer事件也会进行,但它们只是看上去是同时进行,实际上无论Mode怎么变化,它始终都是在一个线程上循环往复。

在iOS开发中,有一个非常重要的原则:永远不能阻塞主线程。因此你的timer事件中不能出现耗时操作,比如改成这样,你会发现你的UI界面会卡顿:

@objc func run() {num += 1print(Thread.current ,num)Thread.sleep(forTimeInterval: 3)//让当前线程休眠
}

Runloop实现的功能

1.自动释放池

自动释放池(AutoreleasePool)的创建、释放、销毁时机如下:

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

2.硬件事件响应

当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。SpringBoard是iOS的系统进程,接收按键(锁屏/静音等)。

然后,SpringBoard 通过 mach_port 将事件传递给目标 App 的进程。此时App 进程中的 RunLoop 会监听来自系统的这些事件。当事件到达时,RunLoop 中注册的 Source1 事件源被触发,唤醒 RunLoop,并执行回调来处理这些事件。

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

3.手势识别

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

这里可以发现:手势识别是在Runloop即将进入休眠的时候进行处理的,这样保证了手势识别的优先级比较高。

4.界面更新

 当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。苹果注册了一个 Runloop的Observer 监听BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

界面更新是在Runloop进入休眠前进行的,这样可以避免用户交互时立即刷新页面。

5.定时器

• NSTimer 是基于 RunLoop 的定时器,底层实现为 CFRunLoopTimerRef。当你创建一个 NSTimer 并将其添加到 RunLoop 中时,RunLoop 会为定时器注册好未来的触发时间点。

• 定时器回调并不总是会在非常准确的时间点触发,因为 RunLoop 会对定时器进行一些优化。定时器有一个 tolerance 参数,允许指定触发时间的最大误差。这样可以在保证性能的前提下,避免系统资源的浪费。

如果在某个时间点执行了一个较长的任务,RunLoop 可能会错过该时间点,直接跳到下一个时间点,而不会延后执行。例如,如果 NSTimer 设置为每 10 秒触发一次,但是某次因为执行长时间任务导致错过了 10:00 的触发点,那么定时器会直接在下一个 10:10 的时间点触发。

监听Runloop

如果我们想知道Runloop何时开始,何时结束,可以通过Observer监听Runloop.

如果你只想监听Runloop的某一个状态(比如即将进入休眠),可以如下创建:

       // 获取当前的 Runlooplet runloop = CFRunLoopGetCurrent()// 需要监听 Runloop 的哪个状态let activities = CFRunLoopActivity.beforeWaiting.rawValue// 创建 Runloop 观察者let observer = CFRunLoopObserverCreateWithHandler(nil, activities, true, 0) { [weak self] (ob, ac) in。。。。//这里写监听到Runloop某种状态后执行的操作}}// 注册 Runloop 观察者CFRunLoopAddObserver(runloop, observer, .defaultMode)

如果要监听所有状态:

enum RunloopError: Error {case canNotCreate
}      do {let block = { (ob: CFRunLoopObserver?, ac: CFRunLoopActivity) inif ac == .entry {print("进入 Runloop")}else if ac == .beforeTimers {print("即将处理 Timer 事件")}else if ac == .beforeSources {print("即将处理 Source 事件")}else if ac == .beforeWaiting {print("Runloop 即将休眠")}else if ac == .afterWaiting {print("Runloop 被唤醒")}else if ac == .exit {print("退出 Runloop")}}let ob = try createRunloopObserver(block: block)CFRunLoopAddObserver(CFRunLoopGetCurrent(), ob, .defaultMode)}catch RunloopError.canNotCreate {print("runloop 观察者创建失败")}catch {}
}fileprivate func createRunloopObserver(block: @escaping (CFRunLoopObserver?, CFRunLoopActivity) -> Void) throws -> CFRunLoopObserver {
//创建监听所有状态的观察者let ob = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, CFRunLoopActivity.allActivities.rawValue, true, 0, block)guard let observer = ob else {throw RunloopError.canNotCreate}return observer}
}

Runloop的应用

线程保活

当子线程执行完任务后,子线程会被销毁,而频繁的线程创建和销毁会导致资源的浪费,所有我们在然后在开启子线程后保证子线程永远活着,这时候就需要常驻线程,即:给线程开启一个Runloop。

优化卡顿

在日常开发中,如果我们要加载一个tableView,一般分两部分进行处理:

1.在子线程中处理cell中呈现的数据资源,其中包括网络请求、数据解析、构造模型等等。

2.模型数组准备完毕,在主线程中刷新tableView,使用模型数据填充cell。

这个思路看似很完美,其实还是会有问题,就是如果第二步,也就是使用模型数据填充cell时十分耗时,怎么办?

简单来说,就是:前面讲Runloop功能时,有一点是渲染UI,假设我们在滑动tableView,这时会触发屏幕上的UI变化,而UI变化会触发Cell的复用和渲染,但其实,Cell的渲染是一个是否耗时的操作,这样就会导致Runloop循环一次的时间变长,造成UI卡顿。这个问题该如何优化呢?

解决思路:将Cell的渲染操作剥离出来。

具体过程如下:

1.用一个block数组,存放渲染Cell的代码

2.在cellForRowAtIndexPath代理方法中直接返回cell。

3.监听Runloop,在即将进入休眠阶段取出block数组中的代码进行执行。

简单来说,就是利用runloop的休眠时间来处理Cell的渲染操作。

参考:

https://github.com/miaoqiu/RunLoop?tab=readme-ov-file#按照官方文档source的分类

https://github.com/tianziyao/Runloop

iOS--RunLoop原理_ios 定时器与runloop-CSDN博客

Runloop解析_objective-c runloop-CSDN博客

http://www.lryc.cn/news/500529.html

相关文章:

  • SpringBoot的Bean类三种注入方式(附带LomBok注入)
  • 开源向量数据库介绍说明
  • 【前端】深度解析 JavaScript 中的 new 关键字与构造函数
  • 2024年华中杯数学建模C题基于光纤传感器的平面曲线重建算法建模解题全过程文档及程序
  • 使用 `typing_extensions.TypeAlias` 简化类型定义:初学者指南
  • 如何快速批量把 PDF 转为 JPG 或其它常见图像格式?
  • 如何在组织中塑造和强化绩效文化?
  • OllyDbg、CE简单介绍
  • Python函数——函数的返回值定义语法
  • 【Pandas】pandas isna
  • mysql 数据库表的大小
  • (6)JS-Clipper2之ClipperOffset
  • 如何在Ubuntu中利用repo和git地址下载获取imx6ull的BSP
  • Ruby On Rails 笔记5——常用验证下
  • JS听到了因果的回响
  • 【高中生讲机器学习】28. 集成学习之 Bagging 随机森林!
  • 硬件设计 | Altium Designer软件PCB规则设置
  • 【Elasticsearch】实现用户行为分析
  • python字符串处理基础操作总结
  • 电子商务人工智能指南 6/6 - 人工智能生成的产品图像
  • 【论文阅读】相似误差订正方法在风电短期风速预报中的应用研究
  • 贪心算法 - 学习笔记 【C++】
  • 精确的单向延迟测量:使用普通硬件和软件
  • 【MySQL 进阶之路】存储引擎和SQL优化技巧分析
  • vue+elementUI从B页面回到A页面并且定位到A页面的el-tabs的某个页签
  • {结对编程/大模型} 实践营项目案例 | 基于RAG搭建政策问答智能聊天助手
  • 【Canvas与图标】乡土风金属铝边立方红黄底黑字图像处理图标
  • 【开源】A064—基于JAVA的民族婚纱预定系统的设计与实现
  • C++实现一个经典计算器(逆波兰算法)附源码
  • Python知识分享第二十二天-数据结构入门