『底层探索』3 - 揭开 isa 神秘的面纱
对于每个 Objective-C 对象中,都存在一个 isa 指向了该对象所属的类,这个类存储了类的相关信息,今天来探索一下 isa 究竟是啥?
基础知识
Bit field
Bit-field
,称之为位段或位域,是一种数据结构,它的数据以位的形式紧凑存储的,并且允许程序员对该结构的位进行操作。
这种数据结构的优点如下:
- 1、节省存储空间。
- 2、可以直接定义和访问一个字中的位字段。
如何定义一个位域呢?
1 | struct 位域结构名 { |
举个例子:
1 | struct Direction { |
那么位域是如何使用的呢?同时,我们定义一个同样形式的结构体,用于对比两者的内存占用大小。
1 | struct Direction1 { |
1 | struct Direction d; |
上面代码打印结果如下:
1 | Direction size -> 4 |
对比结果很明显,使用位域可以显著减少内存空间的占用。 关于位域的更多知识可以看看 Bit field。
union
union
称为联合体或共用体,是一种有多种类型的值的数据结构。联合体中的各个元素是互斥的,它们共享同一块内存。因此一个联合体中每次只能使用其中的一个元素。更多信息参考 union
如下,我们声明一个联合体,我们可以通过 .
进行取值和赋值。
1 | union Data { |
一个联合体占用的内存大小,是联合体中最宽元素的字节大小。Data 中的最宽元素是 double,占用 8 个字节内存大小。可以通过下面的代码验证:
1 | union Data d; |
打印结果为:
1 | Data size = 8 |
位运算
数据在计算机中是以二进制存储的。位运算就是直接对整数在内存中的二进制位进行操作。常用的操作有:
&
与|
或~
取反<<
左移>>
右移
这里我们举例说明一下左移和右移,其他的比较简单,就不赘述了,详细信息可以参考 位操作。
移位操作符用于将一个二进制数中的每一位全部都向一个方向移动指定位,溢出的部分会被舍弃,空缺的部分会被填入指定数字。算数移位中填充的是符号位,逻辑移位中填充的是 0 。
拿数字 138 来举例吧,转换成二进制是 1000 1010
。我们分别进行逻辑左移两位和逻辑右移两位来得到 a 和 b,看 a 和 b 打印的是否如我们分析的那样。
1 | unsigned char val = 138; // 1000 1010 140 |
打印结果如下:
1 | a = 40 |
开始探索
先声明一个简单 RDPet 类, 有一个 String 类型的 name 属性。RDPet.h
和 RDPet.m
文件中的代码如下所示。
1 | @interface RDPet : NSObject |
用 Objective-C 编写的代码,会被 Clang 和 LLVM 编译成机器码,然后运行在我们的计算机上。clang 是 Apple 开发的一个 C、C++、Objective-C、Objective-C++ 编程语言的编译器前端,LLVM 是编译器后端。更多的消息参考 Clang 和 LLVM。
我们使用 Clang rewrite RDPet.m
中的代码转换成 c++ 格式的代码,然后看看会变成啥。
1 | clang -rewrite-objc RDPet.m -o RDPet.cpp |
在终端中蹦出了一系列的 warnings 后,我们得到了一个 RDPet.cpp 文件,用 Xcode 打开这个文件瞅瞅,如图居然有 11 万多行。
我们用 RDPet 关键词搜一下,可以找到这样的代码, 可见我们的 RDPet 类在底层转换成了 struct RDPet_IMPL
。
1 |
|
那么 NSObject_IMPL 是啥呢?再搜一下这个,找到了如下的代码:
1 | struct NSObject_IMPL { |
同时在许多熟悉的 struct 中 也找到了这个属性,这与我们前面的 alloc 文章不谋而合,这就是 isa, 用于存储每个对象所属的类的类信息。
接着打开我们的 runtime 源码,找找这个 Class 的定义。找到如下的声明:
1 | typedef struct objc_class *Class; |
顺藤摸瓜继续找 objc_class。
1 | struct objc_class : objc_object { |
看看 objc_class
的父类 objc_object
是啥?
1 | struct objc_object { |
根据 objc_class
和 objc_object
的关系,我们可以得出一个结论,类也是一个对象。
在 objc_object
中,isa 的类型是 isa_t
,这是什么类型呢?
1 | union isa_t { |
通过源码可以得知,isa_t
是一个联合体类型,联合体中有三种元素,分别是 cls、bits、位域。这 3 个元素是互斥的,它们共享同一块内存区间。
- cls 是一个指向
objc_class
的指针。 - bits 是一个长度为 8 字节的无符号整型数据。
- 位域存储的是类信息。
看看这个位域中的元素是啥, 已经提示在 isa.h 中了。
1 |
|
可以看出,这个位域的元素,根据 CPU 的架构不同,使用的定义也是不同的。根据注释可以很好的明白每个元素代表的含义。
前面的的三个常量也比较有意思:
ISA_MASK
的特点是shiftcls
对应的二进制位都是 1,isa & ISA_MASK
可以快速得到shiftcls
的值。ISA_MAGIC_MASK
的特点是magic
和nonpointer
对应的二进制位都是 1,通过与操作可以快速取值。ISA_MAGIC_VALUE
的特点是nonpointer
是 1,在 isa 的初始化中会用到。
我们将 arm64 架构的 ISA_MAGIC_MASK
和 ISA_MAGIC_VALUE
转成二进制,看看他们的二进制位的关系, 相信你能看出他们的奥妙。
1 | M 0b0000000000000000000000111111000000000000000000000000000000000001 |
实际操作
本文创建的 demo 是 mac OS 项目,所以使用的是 \_\_x86_64__
架构。
如下所示,我们创建一个 RDPet 对象,然后让 5 个 RDPet 类型指针指向这个对象,此时该对象的引用计数是 5,然后分析一下 isa 中的数据是否也是这样的。
1 | RDPet *dog = [[RDPet alloc] init]; |
在 Log 地方打上断点。在 debug area
使用 lldb
指令进行打印。这里对常用的 lldb
指令做一下说明。在 debug area 中输入 help
可以查看完整的 lldb
命令列表。
1 | p 打印返回值 (LLDB默认格式) |
接下来我们探索 isa 中的数据了。先看看 dog 对象的内存地址是什么?
1 | (lldb) p dog // 查看 dog 对象地址 |
用 x/4gx
指令,以 16 进制的方式读取地址 0x0000000101977fc0
开始的 4 段数据,其中 8 字节为一段。
- 4 表示读 4 段数据,这里可以是你想读的数据的段数。
- g 表示以 8 字节为一段。 g 是
giant word
缩写。w 是word
的缩写,如果是 w,则是 4 字节为一段。 - x 表示以 16 进制方式。二进制用 t 表示,八进制用 o 表示。
1 | (lldb) x/4gx 0x0000000101977fc0 // 读取该地址起的4个8字节数据 |
- 冒号左边的数
0x101977fc0
表示的是内存地址编号。 - 冒号右边的表示的是,这个地址开始,在内存中存储的数据。
1、读取 isa 的值。
1 | (lldb) p 0x041d80010000340d // 读取 isa 的值 |
2、看看内存中第 2 个 8 字节的内容是啥?应该是对象的属性。
1 | (lldb) po 0x0000000100002020 // 读取 name 属性值 |
3、读取 isa 数据。通过 &
的方式,读取第一位 bit 的值。这位表示的是否是是 nonpointer。
1 | (lldb) p 0x041d80010000340d & 0x0000000000000001ULL >> 0 // 读取 nonpointer |
4、看是否有关联对象?has_assoc
位是在第 2 位, &
出来的值,需要抹掉第一位的 0,所以需要右移 1 位 来实现。
1 | (lldb) p (0x041d80010000340d & 0x0000000000000002ULL) >> 1 // 读取has_assoc |
5、是否有 c++ 析构函数? has_cxx_dtor
位是在第 3 位。
1 | (lldb) p (0x041d80010000340d & 0x0000000000000004ULL) >> 2 // 读取 has_cxx_dtor |
6、对象的类信息 shiftcls 用 44 位来存储,是在 4-47 位。
1 | (lldb) po 0x041d80010000340d & 0x00007ffffffffff8ULL // 读取 shiftcls, 类信息 |
7、magic value 用 6 位来存储,是在 48-53 位。
1 | (lldb) p (0x041d80010000340d & 0x001f800000000001ULL) >> 47 // 读取 magic |
8、对象是否有弱引用 weakly_referenced 用 1 位类存储,是在 54 位。
1 | (lldb) p (0x041d80010000340d & 0x0002000000000000ULL) >> 53 // 读取 weakly_referenced |
9、对象是否正在析构 deallocating 用 1 位存储,是在 55 位。
1 | (lldb) p (0x041d80010000340d & 0x0004000000000000ULL) >> 54 // 读取 deallocating |
10、对象是否有辅助引用计数表 has_sidetable_rc 用 1 为存储,是在 56 位。
1 | (lldb) p (0x041d80010000340d & 0x0008000000000000ULL) >> 55 // 读取 has_sidetable_rc |
11、对象的引用计数-1 extra_rc 用 8 位表示,是在 57-64 位。
1 | (lldb) p (0x041d80010000340d & 0xff00000000000000ULL) >> 56 // 读取 extra_rc |
实操打印出来的结果和我们的分析是一致的。通过这段分析,可以看出 Apple 的工程师在底层为了内存的高效实用也是花费了不少心思。
后记
我是穆哥,卖码维生的一朵浪花。我们下次见。
文章作者:muhlenXi
原始链接:https://muhlenxi.com/2020/09/10/075-isa/
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 CN 许可协议。转载请注明出处!