【OC源码】Runloop | 一. Runloop 是什么

参考文章:
Run Loops
深入理解RunLoop

我对 runloop 源码的解析:
数据结构源码解析
CFRunloopRun 源码解析
一些其他函数解析

我所使用的源码版本:
CF-1151.16.tar


Runloop 是什么

Runloop 其实是一个比较熟悉的概念, 很多时候我们理解其为 ”让程序不结束的循环”.
以函数做类比, 一个函数从{开始到}结束, 是一个线性的流程. 而我们如果希望函数不是线性的从上到下执行并直接结束, 这时候就会加入控制流程的for,while.让函数的其中一部分, 被条件语句控制所循环, 直到条件时允许结束.

Runloop 核心就是这么一个循环, 目的是让程序一直处于运行状态, 同时 Runloop 还能兼顾以下事情:

  • 需要时, 能处理各种输入的事件
  • 不需要时, 休眠以避免资源浪费
  • 让外界知道当前的状态

在 iOS 中, Runloop 分成两层:

可以理解为 iOS 中的 Runloop 是封装了一些属性, 并且具有循环机制的集合.
(CFRunloop 是结构体 + 函数, 线程安全; NSRunLoop 是类-对象, 非线程安全)
其有以下特性:

  • Runloop 的管理 不是全自动 的, 需要我们自己设计线程代码,并在合适的时机启动.
  • 不需要 明确的创建 Runloop 对象, 每个线程, 都有关联的 Runloop
  • 除了主线程外的线程, 其 Runloop 需要 手动获取,并手动启用

NSRunloop 主要是对 CFRunloop 的封装, 提供面向对象的 API, 后续主要都以CFRunloop为例讲解代码
会分别介绍其封装的内容 – 数据结构, 其循环的执行逻辑 – RunloopRun

Runloop 中有什么

纵观整个 Runloop 其主要包含以下这些概念:

  • Thread
  • Runloop
  • Mode
  • Item:
    • Source
    • Timer
    • Observer

我们先看张官方的图:

可以看到在一个线程(Thread)中, 有一个循环, 这个循环会被两个输入源影响:

  • Input source: 发送异步的事件, 一般来自于其他的线程或者其他应用
  • Timer: 发送同步事件, 固定时间间隔的事件

而除了输入源外, Runloop 还会生成关于当前 Runloop 行为的通知(注册成为 run-loop observers 既可接受这些消息)
苹果为这些 Source, Observer, Timer 设计了一个集合概念 Mode

Thread

iOS 不允许直接为某个线程创建其 Runloop, 而我们能做的, 是调用暴露的接口 获取 对应线程的Runloop

  • [NSRunloop currentRunloop]
  • CFRunLoopGetCurrent()
    其中CFRunLoopGetCurrent() 主要是调用 _CFRunLoopGet0, 其具体代码分析放在这里 _CFRunLoopGet0
    主要流程为:
    1. 对入参(thread), 若为空, 自动将线程设为主线程
    2. 判断全局是否初始化, 若无,则初始化 dictmainrunloop
    3. threaddict 中取 loop
    4. 若取不到, 创建一个 loop 并加入dict 然后返回

结合官方的 runloop 文档可以得到以下结论:

  • 有个全局字典管理 Runloop
  • Runloopthread 一一对应
  • 除了主线程, Runloop 在获取时才会创建, 不然 Thread 默认没有 runloop
  • 主线程的 Runloop 会自动创建, 并自动执行

RunLoop Mode

是多个输入源(input source), 多个定时器(Timer), 多个监听者(Observer)的集合.

  • Mode 的好处在于, 可以让不同类型的 Source 互不影响(比如页面滑动不受干扰)
  • 每次执行 Runloop 都需要显式或隐式的指定一个 Mode.
  • Runloop 执行过程中, 只有与当前 Mode 有关的源能发送他们的消息(Observer同样)
  • 其他 Mode 下的 Source 会挂起, 直到 Runloop切换到对应的 Mode
  • Mode 中如果没有 Source/Timer 其会立即结束

mode 常见的选项:

  • NSDefaultRunLoopMode / KCFRunLoopDefaultMode: 默认模式
  • UITrackingRunLoopMode: 为了保证 scrollview 滚动不受影响的模式
    (通常加入 NSTimer 没有响应就是因为在这个模式中)
  • 两新增的: NSConnectionReplyMode & NSModalPanelRunLoopMode
  • NSRunLoopCommonModes(NSRunLoop)/kCFRunLoopCommonModes(CFRunLoop

通用模式, 这是个重点
首先, 这是个集合. runloop 提供一个叫 common的集合.
这个集合很特别, 他不是”某个”模式(可以理解为是个标签), 所有模式都可以被标记为 common (只要加入这个集合即可)
配合 CommonModes 还有个 CommonItems 的概念(source、timer、observer 的集合)
所有被放入 Common 中的 Mode, 都会共享 CommonItems 中的 SourceTimer/Observer

runloop 在执行过程中, 会调用一些函数执行 source, timer, observer, 这些函数执行过程中, 会判断他们所属的 mode 是否是 common, 以及当前 mode 是否是 common, 如果符合就会执行
会发现代码中这些RunloopDo.. 都会有类似的语句
具体代码解析在: 其他: 一些其他函数解析(CFRunLoopDoObservers__CFRunLoopDoBlocks__CFRunLoopDoSources0)

Source

前面提到过 Input Source, 以异步方式向线程传递事件, 其有两种类型

  • Port-Based Sources: 基于 Mach Port 的输入源, 系统(kernel)自动唤起
    (Mach 属于系统底层微内核, kernel 是底层系统的名字, 详见 其他: Mach 是什么
  • Custom Input Source: 由用户自定义输入源, 必须手动在其他线程唤起

Port-Based Sources:

基于端口输入源, 系统提供了NSPort可以用来创建基于端口的输入源, 不必创建端口输入源, 只需要创建一个端口, 并用NSPort的方法将其添加到 runloop 中, 对象会自动处理输入源的创建和配置

Custom Input Sources:

自定义输入源, 必须使用CFRunloopSourceRef的相关函数,对其配置多个 callback 函数. CF 会在不同的时机调用以配置输入源,处理事件, 以及移出 runloop. 具体调用参考其文档
CFRunLoopSource

Cocoa Perform Selector Sources:

苹果的自定义源, 允许在任何线程中 performselector (就是我们很常见的那个接口), 其在执行完后会被从 runloop 中删除.
本质上是Custom Input Sources

上面这些概念, 对应到数据结构中被分为两个字段:

  • source 1: 既上面的 Port-Based Sources
  • source 0: 既上面的Custom Input Sources & Cocoa Perform Selector Sources
    (具体在 CFRunLoopSourceRef , 1 会自动唤醒,0 需要手动唤醒)

Timer

计时器, 基于时间触发, 在预设的时间同步发一个消息.
是一种线程通知自身的方式
Timer Source 不在当前 mode 时, 其直到 runloop 以其支持的 mode 执行时才会触发, 如果 runloop 正在执行,而 timer 到了, 会等到下一次时间点再触发.
配置文档Configuring Timer Sources

注意,文档中各有这一句Although it generates time-based notifications, a timer is not a real-time mechanism
这个 timer 只是一种基于时间的触发器, 但是其本身,不是实时机制

Observer

Observers 监听着 Runloop 不同状态, 并响应做出响应的集合.
具体的触发监听者的方式是, runlooprun 函数中, 在不同的位置, 有调用监听不同状态的 Observer 的操作
具体可以看这一块源码解析: 其他: CFRunloopRun 源码解析

按文档的说明,触发会发生在:

  • runloop 入口
  • 准备处理 Timer(看源码, 实际没开始)
  • 准备处理 input source
  • 准备休眠
  • 被唤醒时, 但还没执行唤起程序时
  • 离开 runloop 时