IOS开发 Runloop机制

一、Runloop的概念

Runloop是维护事件循环来对事件/消息进行管理的对象,runloop提供了一个函数,线程只要执行了这个函数,就会一直处于这个函数内部 "接受消息->等待->处理" 的循环中,没有消息需要处理时,runloop就休眠,不占用CPU,有消息处理时,runloop就立刻被唤醒。

总结:runloop(死循环)保证runloop所在的线程不退出

二、为什么要有runloop?

1、让线程一直活着接受用户输入

2、决定线程在什么时候应该处理哪些事件

3、发送事件的一方不能被接受事件的一方卡住,他会连续发送事件,所以要有消息队列

4、没有事件时,线程会处于阻塞态,不占用CPU,从而节省CPU时间

  • CFRunLoopRef是在Core Foundation框架里的,它提供了纯C函数的API,所有这些API都是线程安全的
  • NSRunLoop是CFRunLoopRef的封装,提供了面向对象的API,这些API都不是线程安全的。

三、数据结构

__CFRunLoopRun内部其实是一个_do while_循环,这也正是Runloop运行的本质。执行了这个函数以后就一直处于"等待-处理"的循环之中,直到循环结束。只是不同于我们自己写的循环它在休眠时几乎不会占用系统资源,当然这是由于系统内核负责实现的,也是Runloop精华所在。

CFRunLoopRefCFRunloopModeCFRunLoopSourceRef/CFRunloopTimerRef/CFRunLoopObserverRef关系如下图:

一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

1、Runloop Mode

  • 默认模式(NSDefaultRunLoopMode):一般处理timer、网络等事件。
  • UI模式(优先级最高)(UITrackingRunLoopMode):专门处理UI事件,只有触摸事件才能唤醒UI模式,当UI模式和默认模式同时有事件发生时,runloop会去处理UI模式下的事件
  • UI模式 + 默认模式 (NSRunLoopCommonModes):占位模式,在iOS系统中默认包含了NSDefaultRunLoopMode和 UITrackingRunLoopMode(注意:并不是说Runloop会运行在kCFRunLoopCommonModes这种模式下,而是相当于分别注册了 NSDefaultRunLoopMode和 UITrackingRunLoopMode。

2、Source

Source结构体类型:事件源,任意事件本质都是source,source是RunLoop的数据源抽象类

按照函数调用栈分为

1、source0:非source1就是source0,负责App内部事件,由App负责管理触发,例如UITouch事件,Source0是Input Source中的一类,Input Source还包括Custom Input Source,由其他线程手动发出,RunLoop被这些事件唤醒之后就会处理并调用事件处理方法(CFRunLoopTimerRef的回调指针和CFRunLoopSourceRef均包含对应的回调指针)。

2、source1:系统内核事件的处理,Source1可以监听系统端口和其他线程相互发送消息,它能够主动唤醒RunLoop(由操作系统内核进行管理,例如CFMessagePort消息),source0不能主动唤醒runloop。

3、Observer

observer:观察runloop,向外部报告runloop的几种状态(它包含一个函数指针_callout_将当前状态及时告诉观察者),框架中的很多机制都是由RunLoopObserver触发,比如CAAnimation,CAAnimation是在runloop要睡的时候调用,把这一圈的动画事件全部汇集起来,最后去调用。

4、timer

5、Call out

在开发过程中几乎所有的操作都是通过Call out进行回调的(无论是Observer的状态通知还是Timer、Source的处理),而系统在回调时通常使用如下几个函数进行回调(换句话说你的代码其实最终都是通过下面几个函数来负责调用的,即使你自己监听Observer也会先调用下面的函数然后间接通知你,所以在调用堆栈中经常看到这些函数):

objectivec 复制代码
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FOUNDTION__


__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK


__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE


__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__


__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__


__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM__FUNCTION__

6、RunLoop休眠

其实对于Event Loop而言RunLoop最核心的事情就是保证线程在没有消息时休眠以避免占用系统资源,有消息时能够及时唤醒。RunLoop的这个机制完全依靠系统内核来完成,

当程序静止时,RunLoop停留在__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy),而这个函数内部就是调用了mach_msg(本质上是一个系统调用)让程序处于休眠状态。

四、Runloop运行流程

一、核心执行阶段

1. 前置通知 + 高优先级事件处理(先干活,再休眠)

  • 第一步:通知观察者 "准备处理事件" 先给当前 Mode 的所有 Observer 发通知(如 kCFRunLoopBeforeWaiting),让外部框架(如 UIKit)提前准备(比如刷新界面状态)。
  • 第二步:优先处理 "到期 Timer" 只触发当前 Mode 下已到时间的 Timer(如 NSTimer),未到期的留到后续迭代。
  • 第三步:处理 "就绪 Source0" 执行用户态事件(如 performSelector 任务、UI 点击预处理),但 Source0 不能主动唤醒 RunLoop,需靠其他事件(如 Source1)唤醒后顺带处理。

2. 内核事件检查(Source1 是 "紧急唤醒键")

  • 检查当前 Mode 的 Source1(内核态事件,如跨线程消息、系统端口消息)是否就绪:
    • 若就绪 :立即执行其回调,处理完后必须 "回退" 到 "处理 Timer" 步骤(避免遗漏因 Source1 唤醒后新到期的 Timer / 新就绪的 Source0);
    • 若未就绪:进入休眠流程。

3. 休眠 - 唤醒 - 定向处理(无活就休眠,被唤醒就精准干活)

  • 休眠前:通知观察者 "要休眠"kCFRunLoopBeforeWaiting 通知,告知外部 "RunLoop 即将释放 CPU 休眠"。
  • 休眠中:等待被唤醒 线程暂停,仅被 4 类事件唤醒:新 Source0 就绪(需手动 CFRunLoopWakeUp)、Source1 触发、Timer 到期、外部 CFRunLoopStop
  • 唤醒后:先通知,再定向处理
    1. kCFRunLoopAfterWaiting 通知,告知 "已唤醒";
    2. 按 "唤醒原因" 精准处理:
      • Timer/Source1 唤醒:回退到 "处理 Timer" 步骤,重新扫事件;
      • Source0 唤醒:直接去 "处理 Source0";
      • CFRunLoopStop 唤醒:标记 "退出",结束本次迭代。

二、循环判断:是否继续下一轮迭代

  • 每次处理完事件后,检查两个退出条件:
    1. 是否收到 CFRunLoopStop 信号;
    2. 当前 Mode 下是否无任何可处理的 Source/Timer
  • 满足任一条件:停止 RunLoop;
  • 不满足:回到 "前置通知" 步骤,开始下一次迭代。

五、Runloop与NSTimer

当触摸事件发生时,timer事件不显示,因为把timer添加到默认模式了,解决办法是把timer添加到commonMode模式。

六、Runloop与线程的关系

  1. 线程是和RunLoop一一对应的。
  2. 自己创建的线程默认是没有RunLoop的。
  3. 主线程的RunLoop默认是开启的。

问题:怎么样实现一个常驻线程

  1. 为当前线程开启一个RunLoop、
  2. 向该RunLoop中添加一个Port/Source等维持RunLoop的事件循环。
  3. 启动该RunLoop。

1、RunLoopObserver和Autorealease Pool的关系

UIKit通过RunLoopObserver在RunLoop两次Sleep间 (runloop的一圈)对AutoreleasePool进行Pop和Push将这次Loop中产生的Autorelease对象释放,把自动释放池中的对象进行释放。

2、UI更新

当UI改变( Frame变化、 UIView/CALayer 的继承结构变化等)时,或手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理。

苹果注册了一个用来监听BeforeWaiting和Exit的Observer,在它的回调函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

相关推荐
從南走到北3 小时前
JAVA国际版任务悬赏发布接单系统源码支持IOS+Android+H5
android·java·ios·微信·微信小程序·小程序
咕噜签名分发冰淇淋3 小时前
苹果ios安卓apk应用APP文件怎么修改手机APP显示的名称
android·ios·智能手机
游戏开发爱好者84 小时前
iOS 开发推送功能全流程详解 从 APNs 配置到上架发布的完整实践(含跨平台上传方案)
android·macos·ios·小程序·uni-app·cocoa·iphone
Larva5 小时前
iOS - 关于如何在编译时写入文件并在代码内读取文件内容
ios
胎粉仔17 小时前
Objective-C 初阶 —— __bridge & __bridge_retained & __bridge_transfer
ios·objective-c
笑尘pyrotechnic17 小时前
【OC】UIKit常用组件适配iOS 26
macos·ios·cocoa
ii_best1 天前
按键精灵安卓/iOS脚本辅助,OpenCV实现自动化高效率工具
ios·自动化·编辑器·安卓
蒲公英少年带我飞1 天前
iOS底层原理:Method Swizzling原理和注意事项
ios