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运行模式
RunLoop优先处理UI模式的事件,而UI模式只能被UI事件唤醒
- NSDefaultRunLoopMode 默认模式 —— 一般处理timer\网络事件
- UITrackingRunLoopMode UI模式 —— 专门处理UI事件
- NSRunLoopCommonModes 占位模式( UI && 默认)
- UIInitializationRunLoopMode 在刚启动App时第进入的第一个Mode,启动完成后就不再使用
- 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
方法。
事件循环的时间机制
- main函数—> UIApplicationMain
- 在UIApplicationMain中启动主线程的Runloop
- 即将进入Runloop(通知observer)
- 将要处理timer、source0事件(通知observer)
- 处理source0事件
- 如果有source1事件要处理(跳转到10)
- 线程将要休眠(通知observer)
- 休眠、等待唤醒(唤醒的方法:1 source1事件,2 Timer事件,3 外部手动唤醒)
- 线程刚被唤醒(通知observer)
- 处理唤醒时收到的消息
- 即将退出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
还提供了其他常用操作,例如:
stop
:停止RunLoop
的运行。runUntilDate
:运行RunLoop
直到指定日期。runMode
:运行RunLoop
指定模式下的事件处理循环。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
的线程安全,可以使用以下方法:
- 使用
RunLoopQueue
,在队列中使用RunLoop
来执行异步操作。 - 在主线程中使用
RunLoop
来处理异步事件,避免跨线程操作
q1: RunLoop可以做什么?
- 处理Crash(程序崩溃不退出)
- 保持线程存活(线程保活)
- 监测和优化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怎么启动
- run
- run(until)
- 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)]