Links

06.WWDC20-runtime优化

其中主要提到了三点优化
  • Class数据结构的变化
  • 方法列表的变化
  • Tagged pointer 格式变化

一、Class数据结构与运行时

1.1 class_ro_t

在二进制文件中,Class的结构是这样的
Class
首先Class对象包含了最常用的信息:指向元类、父类、方法缓存的指针
他还有一个指向更多数据的指针,存储这些额外信息的地方是:class_ro_t
class_ro_t
RO 代表 Read only, 它包含:
  • 类名
  • 方法
  • 协议
  • 实例变量信息等
Swift ClassOC Class 共享这一数据结构
当类第一次从磁盘加载到内存中时,他的结构是这样的,但是一经使用,他就会发生变化

1.2 Clean memory & Dirty memory

  • Clean memory
    • 是指加载后不会发生改变的内存
    • class_ro_t 就是这种
  • Dirty memory
    • 是指进程运行时会发生改变的内存
    • Class对象 就是这种,应为它在运行时会发生变化
CD
Dirty memory 要比 Clean memory 昂贵的多,只要进程在运行,他就一直存在。Clean memory 可以进行移除,以节省空间;如果需要可以再从磁盘中加载。
之所以将Class的数据分成两部分,是为了尽可能的节省空间

1.3 class_ro_t

运行时需要追踪每个类的更多信息,当一个类首次被使用时,运行时会为他分配额外的存储空间。 class_rw_t
RW
class_rw_t 使用来读写数据的,在这个数据结构中存储了只有在运行时才会生成的信息。
RW
方法、属性、协议也在 class_rw_t 中,是因为运行时可以通过 Category 动态添加。因为 class_ro_t 是只读的所以我们就在 class_rw_t 管理这些。
RW
但是这样会占用很多的内存。那么 如何缩小这部分的体积呢?

二、优化 class_rw_t

虽然每个类有这样的数据结构,但是真正用到这部分读写功能的却只占很少一部分。
只有 SwiftDemangled name,并且只有需要访问 Swift类Objective-c 名称时才会用到它。
RW
我们将 class_rw_t 分为了两部分:
  • class_rw_t
    • 常用的
  • class_rw_ext_t
    • 不常用的
RW
这将 class_rw_t 的体积减少了进 50%
在有需要的时候将 class_rw_ext_t 插入其中,约 90% 的类用不到这些拓展数据。
RW

三、方法列表

RW

3.1 方法列表元素的结构

selector 方法名

RW

Type Encodings 类型编码

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

指向方法实现的指针

RW

3.2 方法列表元素的大小

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

3.3 寻址

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

3.4 优化:Relative method lists

2020年runtime做了优化,通过32位内存偏移来进行寻址,简化了寻址操作。
RW
  • 偏移量始终是相同的
    • 无论image从哪里加载到内存中,它们从磁盘加载到内存后无需进行修正
  • 将方法列表存入只读的空间,安全节省内存
  • 使用32位来存储,节省了一半的空间

3.5 Swizzling relative method lists

RW
现在的imp不能引用完整的地址空间,但如果你Swizzle一个方法,他就可以在任何地方实现。
由于方法列表放入了只读的空间,我们并不能直接修改方法列表。这里提供了一个全局表。这个全局表将方法映射到他们被Swizzle的实现上。
RW
在实际情况下,只会有少量的Swizzle所以这个表不会很大。

四、兼容性

4.1 系统要求

支持版本
macOS Big Sur
iOS 14
tvOS 14
watchOS 7
Xcode会对支持更低版本的包按老的方式进行处理。兼容Swift。

4.2 一些SDK

如果我们使用了在其他地方编译好的SDK,但是他又没有兼容 Related method lists
在老的OS下运行,如果它通过地址去访问方法,它会将两个段32位数据作为一个64位去进行读取,两个指针粘连在一起,那么就会发生崩溃。
RW
为保证不会出现这样的问题,请使用系统API。
RW

五、Tagged pointer format changes (ARM64)

5.1 Tagged pointer

RW
以前一个对象的指针是这样的结构。我们只用到了中间的部分。
由于低位对齐的原则低三位为0,由于地址空间有限,高位为0。
Tagged pointer,将第一位设为了1。这是区别它和原始指针的最简单的方式。以 NSNumber 为例:
RW

5.2 Tagged pointer On Intel

RW
  • 最低位1
    • 代表它是 Tagged pointer
  • 接下来3位是:标签位
    • 表示了指针的类型,例如3代表它是 NSNumber
    • 这里有3位,这里最多能有8种类型
  • 剩下的是 Payload 是特定类型可以随意使用的数据
    • 对于NSNumber来说,这里就是实际的数字

对于 标签7 有特殊情况

RW
  • 它表示一个拓展标签
  • 使用接下来的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

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

优化 objc_msgSend

目的是加快 objc_msgSend 最常见的分支路径。
RW
只用通过最高位是否为1,就可以区分同时检查它是 (nil || 非Tagged pointer),节省了条件分支。

优化: iOS 14

RW
在新版系统中,将标签为移到了后3位。
RW
  • ARM64下,会忽略指针的前8位
    • Top byte ignore 的 ARM特性
注意:以前这样的判断是OK的,但是在iOS14后就不行了。
RW
老老实实使用系统API,它会帮你处理一切。不要为了看起来牛逼而写一些这种危机四伏的代码。
RW