在 WWDC2020 中 Objective-C 运行时的改进这个视频提到关于类的的数据结构的一些变化,本文是对这个视频提到的部分变化进行翻译。
如果想了解更详细的内容,请移步去官网看这个视频:developer.apple.com/wwdc20/1016…。
视频中介绍了三个变化:
- 首先是数据结构的变化, Objective-C 运行时会使用它们来追踪类。
- 其次是 Objective-C 方法列表的变化。
- 最后是 tagged pointer 格式的变化。
一、数据结构的变化
类对象本身包含了最常被访问的信息:指向元类、超类和方法缓存的指针,它还有一个指向更多数据的指针,存储额外信息的地方叫做 class_ro_t
。
“ro
”代表只读,它包括像类名词,方法,协议,和实例变量的信息。Swift 类和 Objective-C 类共享这一数据结构,所以每个 Swift 类也有这些数据结构。
当类第一次从磁盘中加载到内存中时,它们一开始也是这样的,但一经使用,它们就会发生变化。
了解这些变化之前,先了解一下 clean memory
和 dirty memory
的区别。
* clean memory
:指加载后不会发生更改的内存。class_ro_t
就属于 clean memory
,因为它是只读的。
* dirty memory
:指在进程运行时会发生更改的内存。类结构一经使用就会变成 dirty memory
,因为运行时会向它写入新的数据。例如,创建一个新的方法缓存并从类中指向它。
dirty memory
比 clean memory
要昂贵得多,只要进程在运行,它就必须一直存在 。另一方面 clean memory
可以进行移除,从而节省更多的内存空间,当需要使用 clean memory
的时候系统可以从磁盘中重新加载。
macOS 可以选择唤出 dirty memory
,但因为 iOS 不使用 swap
,所以 dirty memory
在iOS中的代价很大。
虽然这些数据足以让我们开始,但运行时需要追踪每个类的更多信息,所以当一个类首次被使用,运行时会为它分配额外的存储容量。
这个运行时分配的存储容量是 class_rw_t
用于读取-编写数据,在这个数据结构中,我们存储了只有在运行时才会生成的新信息,First Subclass
,Next Sibling Class
。
例如,所有的类都会链接成一个树状结构,这是通过使用 First Subclass
,Next Sibling Class
指针实现的,这允许运行时遍历当前使用的所有类,这对于使方法缓存无效非常有用。
但为什么方法和属性也在只读数据中时,这里还要有方法和属性呢?
因为它们可以在运行时进行更改,当 category
被加载时,它可以向类中添加新的方法,而且程序员可以使用运行时 API 动态的添加它们,而 class_ro_t
是只读的,所以我们需要在 class_rw_t
中追踪这些东西。
只有 Swift 类会使用 demangled name
字段,并且 Swift 类并不需要这一字段,除非有东西访问它们的 Objective-C 名称时才需要。
所以我们可以拆掉那些平时不用的部分-class_rw_ext_t
,这将 class_rw_t
的大小减少了一半。
对于那些确实需要额外信息的类,我们可以分配这些扩展记录中的一个,并把它滑到类中供其使用。
二、Objective-C方法列表的变化
每一个类都附带一个方法列表,当你在类上编写新方法时,它就会被添加到列表中。运行时使用这些列表来解析消息发送。
每个方法都包含三个信息。
-
首先是方法的名称,或者说选择器,选择器时字符串,但它们具有唯一性,所以它们可以使用指针相等来进行比较。
-
接下来是方法的类型编码 这是一个表示参数和返回类型的字符串 它不是用来发送消息的 但它是运行时
introspection
和消息forwarding
所必需的东西。 -
最后,还有一个指向方法实现的指针,方法的实际代码,当你编写一个方法时,它会编译成一个 c 函数,其中包含你的实施,然后方法列表中的
entry
会指向该函数。
三、tagged pointer
什么是 tagged pointer
呢?0x00000001003041e0
的二进制表示如图所示:
我们把它分解成二进制表示法 我们有 64 位 然而 我们并没有真正地使用到所有这些位。
- 我们只在一个真正的对象指针中 使用了中间的这些位。
- 由于对齐要求的存在 低位始终为 0 对象必须总是位于 指针大小倍数的一个地址中。
- 由于地址空间有限 所以高位始终为 0 我们实际上不会用到 2^64。
0000 0000 0000 0000 0000 0000 0000 0001 0000 0000 0011 0000 0100 0001 0001(0000 变成 0001)
复制代码
- 这些高位和低位总是 0 所以 让我们从这些始终为 0 的位中 选择一个位并把它设置为 1,这可以让我们立即知道 这不是一个真正的对象指针 然后我们可以给其他所有位 赋予一些其他的意义 我们称这种指针为
tagged pointer
。
例如 我们可以在其他位中塞入一个数值 只要我们想教 NSNumber 如何读取这些位 并让运行时适当地处理 tagged pointer
系统的其他部分就可以 把这些东西当做对象指针来处理 并且永远不会知道其中的区别。
这样可以节省我们为每一种类似情况,分配一个小数字对象的代价,这是一个重大的改进。