06.WWDC20-runtime优化

官网链接WWDC20-Advancements in the Objective-C runtime

其中主要提到了三点优化

  • Class数据结构的变化

  • 方法列表的变化

  • Tagged pointer 格式变化

一、Class数据结构与运行时

1.1 class_ro_t

在二进制文件中,Class的结构是这样的

首先Class对象包含了最常用的信息:指向元类、父类、方法缓存的指针

他还有一个指向更多数据的指针,存储这些额外信息的地方是:class_ro_t

RO 代表 Read only, 它包含:

  • 类名

  • 方法

  • 协议

  • 实例变量信息等

Swift ClassOC Class 共享这一数据结构

当类第一次从磁盘加载到内存中时,他的结构是这样的,但是一经使用,他就会发生变化

1.2 Clean memory & Dirty memory

  • Clean memory

    • 是指加载后不会发生改变的内存

    • class_ro_t 就是这种

  • Dirty memory

    • 是指进程运行时会发生改变的内存

    • Class对象 就是这种,应为它在运行时会发生变化

Dirty memory 要比 Clean memory 昂贵的多,只要进程在运行,他就一直存在。Clean memory 可以进行移除,以节省空间;如果需要可以再从磁盘中加载。

之所以将Class的数据分成两部分,是为了尽可能的节省空间

1.3 class_ro_t

运行时需要追踪每个类的更多信息,当一个类首次被使用时,运行时会为他分配额外的存储空间。 class_rw_t

class_rw_t 使用来读写数据的,在这个数据结构中存储了只有在运行时才会生成的信息。

方法、属性、协议也在 class_rw_t 中,是因为运行时可以通过 Category 动态添加。因为 class_ro_t 是只读的所以我们就在 class_rw_t 管理这些。

但是这样会占用很多的内存。那么 如何缩小这部分的体积呢?

二、优化 class_rw_t

虽然每个类有这样的数据结构,但是真正用到这部分读写功能的却只占很少一部分。

只有 SwiftDemangled name,并且只有需要访问 Swift类Objective-c 名称时才会用到它。

我们将 class_rw_t 分为了两部分:

  • class_rw_t

    • 常用的

  • class_rw_ext_t

    • 不常用的

这将 class_rw_t 的体积减少了进 50%

在有需要的时候将 class_rw_ext_t 插入其中,约 90% 的类用不到这些拓展数据。

三、方法列表

3.1 方法列表元素的结构

selector 方法名

Type Encodings 类型编码

这是一个表示参数类型的字符串。官方文档

指向方法实现的指针

3.2 方法列表元素的大小

在64位系统下,占用24字节。

3.3 寻址

下面的图模拟了内存的分布

方法元素的三个指针指向了App内存空间的相对位置,并在加载时修正为实际的位置。由于在内存中,这三个元素的内存地址是连续的。所以我们可以对寻址进行优化。

3.4 优化:Relative method lists

2020年runtime做了优化,通过32位内存偏移来进行寻址,简化了寻址操作。

  • 偏移量始终是相同的

    • 无论image从哪里加载到内存中,它们从磁盘加载到内存后无需进行修正

  • 将方法列表存入只读的空间,安全节省内存

  • 使用32位来存储,节省了一半的空间

3.5 Swizzling relative method lists

现在的imp不能引用完整的地址空间,但如果你Swizzle一个方法,他就可以在任何地方实现。

由于方法列表放入了只读的空间,我们并不能直接修改方法列表。这里提供了一个全局表。这个全局表将方法映射到他们被Swizzle的实现上。

在实际情况下,只会有少量的Swizzle所以这个表不会很大。

四、兼容性

4.1 系统要求

Xcode会对支持更低版本的包按老的方式进行处理。兼容Swift。

4.2 一些SDK

如果我们使用了在其他地方编译好的SDK,但是他又没有兼容 Related method lists

在老的OS下运行,如果它通过地址去访问方法,它会将两个段32位数据作为一个64位去进行读取,两个指针粘连在一起,那么就会发生崩溃。

为保证不会出现这样的问题,请使用系统API。

五、Tagged pointer format changes (ARM64)

5.1 Tagged pointer

以前一个对象的指针是这样的结构。我们只用到了中间的部分。

由于低位对齐的原则低三位为0,由于地址空间有限,高位为0。

Tagged pointer,将第一位设为了1。这是区别它和原始指针的最简单的方式。以 NSNumber 为例:

5.2 Tagged pointer On Intel

  • 最低位1

    • 代表它是 Tagged pointer

  • 接下来3位是:标签位

    • 表示了指针的类型,例如3代表它是 NSNumber

    • 这里有3位,这里最多能有8种类型

  • 剩下的是 Payload 是特定类型可以随意使用的数据

    • 对于NSNumber来说,这里就是实际的数字

对于 标签7 有特殊情况

  • 它表示一个拓展标签

  • 使用接下来的8位来编码类型

  • 这允许多出256个标签类型

  • 但是代价是减少了 Payload 的可用区域

  • 这使得我们可以将 Tagged pointer 用于更多类型,只要 Payload 能够装入。

    • 如:UIColorNSIndexSet

注意

  • 开发者无法添加 Tagged pointer 类型。(OC)

  • Swift中带有关联值的枚举就是一个类似 Tagged pointer 的类。

    • Swift运行时将枚举判别器存储在关联值的 Payload 的备用位中。

    • 而且Swift值类型的运用,使得 Tagged pointer 没有那么重要了。

    • If you've ever used an enum with an associated value that's a class, that's like a tagged pointer. The Swift runtime stores the enum discriminator in the spare bits of the associated value payload.

5.3 Tagged pointer On ARM

ARM 下,结构使反过来的。为什么不合Intel上保持一致呢?

优化 objc_msgSend

目的是加快 objc_msgSend 最常见的分支路径。

只用通过最高位是否为1,就可以区分同时检查它是 (nil || 非Tagged pointer),节省了条件分支。

优化: iOS 14

在新版系统中,将标签为移到了后3位。

  • ARM64下,会忽略指针的前8位

    • Top byte ignore 的 ARM特性

注意:以前这样的判断是OK的,但是在iOS14后就不行了。

老老实实使用系统API,它会帮你处理一切。不要为了看起来牛逼而写一些这种危机四伏的代码。

Last updated