深入理解RunLoop

独奏

学习思考|2017-3-30|最后更新: 2023-2-23|
type
status
date
slug
summary
tags
category
icon
password

事件驱动模型

大部分的 GUI 程序都使用了事件驱动模型,有些服务器(nginx, node.js)也使用了事件驱动模型。相比于轮询等其他方式,其优点在于极大的提高了 CPU 使用率,在没有事件的时候,能够让出 CPU 时间片,来事件时也可以快速的得到响应。

Cocoa 中的 RunLoop

我们常用到的是 NSRunLoop ,其位于 Foundation 框架中。Foundation 框架实际上是 Core Foundation 的部分导出。而 Core Foundation 的源码是开源的。NSRunLoop 实际上是 CFRunLoop 的高层抽象。CFRunLoop 源码可以从这里 下载到。
RunLoop 在 Cocoa 中的应用相当广泛。
NSTimer,UIEvent,NSObject (NSDelayedPerforming),NSObject (NSThreadPerformAdditions),NSURLConnection 是通过 Source 的机制把事件加入到 RunLoop 中。
Autorelease,CADisplayLink,CATransition,CAAnimation 则是利用 observer 的机制,在 RunLoop 的循环周期中执行各自的操作。
更加底层的 GCD,mach kernel 也与 RunLoop 存在协作关系。
使用 Xcode 调试的时候我们经常可以看到 RunLoop 相关的堆栈信息:
notion image
runloop断点
图中可以看到四处关于 RunLoop 的堆栈信息,最上面一条 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ 则是 RunLoop 中的 observer 的 call out 调用。其它的调用还包括:
图中还可以看出 CATransition 利用了 RunLoop 的 observer 做视图的相关操作。

RunLoop 机制

notion image
RunLoop结构
这一张图我们可以看出 RunLoop 的组织结构关系。
下面针对各个部分进行了说明

CFRunLoop

  • CFRunLoop 里面包含了线程,若干个 mode。
  • CFRunLoop 和线程是一一对应的。
  • _blocks_head 是 perform block 加入到里面的

CFRunLoopMode

  • runloop mode 里面包含了source0、source1、timer、observer 各种事件,以及 port 集合。其负责管理这些事件。
CFRunLoopMode 包含下面几种 mode
mode
name
description
Default
NSDefaultRunLoopMode (Cocoa) kCFRunLoopDefaultMode (Core Foundation)
用的最多的模式,大多数情况下应该使用该模式开始 RunLoop并配置 input source
Connection
NSConnectionReplyMode (Cocoa)
Cocoa用这个模式结合 NSConnection 对象监测回应,我们应该很少使用这种模式
Modal
NSModalPanelRunLoopMode (Cocoa)
Cocoa用此模式来标识用于模态面板的事件
Event tracking
NSEventTrackingRunLoopMode (Cocoa)
Cocoa使用此模式在鼠标拖动loop和其它用户界面跟踪 loop期间限制传入事件
Common modes
NSRunLoopCommonModes (Cocoa) kCFRunLoopCommonModes (Core Foundation)
这是一组可配置的常用模式。将输入源与些模式相关联会与组中的每个模式相关联。Cocoa applications 里面此集包括Default、Modal和Event tracking。Core Foundation只包括默认模式,你可以自己把自定义mode用CFRunLoopAddCommonMode函数加入到集合中.

RunLoopSource

RunLoopSource 分为 source0 和 source1。
  • source0 是非基于 port 的事件,主要是 APP 内部事件,如点击事件,触摸事件等。
  • source1 是基于Port的,通过内核和其他线程通信,接收,分发系统事件。
  • __CFRunLoopSource 里面包含一个 _runLoops,也就意味着一个 __CFRunLoopSource 可以被添加到多个 runloop mode 中去。

RunLoopObserver

CFRunLoopObserver 是观察者,可以观察RunLoop的各种状态,并抛出回调。可以监听得状态如下:

RunLoopTimer

  • CFRunLoopTimer 是定时器,可以在设定的时间点抛出回调
  • CFRunLoopTimer和NSTimer是toll-free bridged的,可以相互转换。

CFRunLoopGet

  • RunLoop 和线程是一一对应的
  • 不允许直接创建RunLoop
  • 线程刚创建时并没有 RunLoop, 如果你不主动获取, 那它一直都不会有
  • 你只能在一个线程的内部获取其 RunLoop(main thread 除外)
  • RunLoop 会在线程销毁时销毁
  • RunLoop 和线程的一一对应关系保存一个全局 Dictionary 里 __CFRunLoops
  • 即使主线程,不主动创建也不会有 RunLoop,只是 application 会在主线程默认创建 RunLoop
  • 如果创建子线程 RunLoop,主线程会默认加上一个 RunLoop

CFRunLoopRun

notion image
RunLoopRun
执行过程大致描述如下:
  1. 通知 observers 即将进入 run loop
  1. 通知 observes 即将开始处理 timer source
  1. 通知 observes 即将开始处理 source0 事件
  1. 执行 source0 事件
  1. 如果处于主线程有 dispatchPort 消息,跳转到第9步
  1. 通知 observes 线程即将进入休眠
  1. 内循环阻塞等待接收系统消息,包括:
      • 收到内核发送过来的消息 (source1消息)
      • 定时器事件需要执行
      • run loop 的超时时间到了
      • 手动唤醒 run loop
  1. 通知 observes 线程被唤醒
  1. 处理通过端口收到的消息:
      • 如果自定义的 timer 被 fire,那么执行该 timer 事件并重新开始循环,完成后跳转到第2步
      • 如果 input source 被 fire,则处理该事件
      • 如果 run loop 被手动唤醒,并且没有超时,完成后跳转到第2步
  1. 通知 observes run loop 已经退出
注意:
  • CFRunLoopDoBlocks 是执行 perform block 中的 block
  • 绿色的是RunLoopRun()
  • 第一次循环 CFRunLoopServiceMachPort 是不走的
  • handle_msg 处理 timer 事件,处理 main queue block 事件,处理 source1 事件
  • 中间的红色CFRunLoopServiceMachPort是监听 GCD 的端口事件,只监听一个端口,左下角的CFRunLoopServiceMachPort是坚挺 source1,timer 的,是一个 MutableSet

RunLoop 与 GCD

RunLoop 与 GCD 是互相协作的关系,RunLoop 的最开始部分使用了 GCD 的 timer 做超时的回调;通过 GCD 调用带有 RunLoop 的线程的 block,会通过 dispatch port CFRunLoopServiceMachPort 把事件发送到该线程的 RunLoop 里面。
比如:
主线程存在 runloop,那么 GCD 会通过 dispatch port CFRunLoopServiceMachPort,把事件发送给 RunLoop,RunLoop 接收到时间之后,会执行这个 block。

NSTimer 与 GCD Timer

NSTimer 是通过 RunLoop 的 RunLoopTimer 把时间加入到 RunLoopMode 里面。官方文档里面也有说 CFRunLoopTimer 和 NSTimer 是可以互相转换的。由于 NSTimer 的这种机制,因此 NSTimer 的执行必须依赖于 RunLoop,如果没有 RunLoop,NSTimer 是不会执行的。
GCD 则不同,GCD 的线程管理是通过系统来直接管理的。GCD Timer 是通过 dispatch port 给 RunLoop 发送消息,来使 RunLoop 执行相应的 block,如果所在线程没有 RunLoop,那么 GCD 会临时创建一个线程去执行 block,执行完之后再销毁掉,因此 GCD 的 Timer 是不依赖 RunLoop 的。
至于这两个 Timer 的准确性问题,如果不再 RunLoop 的线程里面执行,那么只能使用 GCD Timer,由于 GCD Timer 是基于 MKTimer(mach kernel timer),已经很底层了,因此是很准确的。
如果在 RunLoop 的线程里面执行,由于 GCD Timer 和 NSTimer 都是通过 port 发送消息的机制来触发 RunLoop 的,因此准确性差别应该不是很大。如果线程 RunLoop 阻塞了,不管是 GCD Timer 还是 NSTimer 都会存在延迟问题。

应用

  • 异步的回调如果存在延时操作,那么就要放到有 RunLoop 的线程里面,否则回调没有着陆点无法执行
  • NSTimer 必须得在有 RunLoop 的线程里面才能执行,另外,使用 NSTimer 的时候会出现滑动 TableView,Timer 停止的问题,是由于 RunLoopMode 切换的问题,只要把 NSTimer 加到 common mode 就好了。
  • 滚动过程中延迟加载,可以利用滚动时 RunLoopMode 切换到 NSEventTrackingRunLoopMode 模式下这个机制,在 Default mode 下添加加载图片的方法,在滚动时就不会触发。

演示代码

代码中演示了 RunLoop 的不同事件机制,也演示了 GCD Timer 和 NStimer 的区别。
源码注释: source

参考