现在对象在内存中已经分配好内存空间了,但对象和类是怎么关联上的呢,这就是 isa
的工作了。
isa 联合体
我们可以看一下对象的表现形式:
struct objc_object { |
所以每一个对象必然有一个 isa
。
然后我们看一下 isa
的结构:
union isa_t { |
可以看到,isa
是一个 联合体 。
isa 的结构
首先我们来分析一下 isa
的结构:
isa
有三个成员:Class
、 bits
和一个结构体。
以下以 64 位架构为例分析:
Class
typedef struct objc_class *Class; |
Class
是一个结构体指针,所以 Class
占 8 字节。
bits
typedef unsigned long uintptr_t; |
bits
是一个无符号长整形,占 8 个字节。
结构体
struct { |
结构体的内容是一个宏定义,宏定义在编译时替换成定义好的内容,这样就可以区分不同架(如 __x86_64__
与 __arm64__
)。
其实这里的实现,就是上面我们提到的位域。
从图中我们可以看出,两个架构上结构体的字段相同,只是分布不同。
结构体的大小为 8 字节,即 64 bit
。
arm64
: 1 + 1 + 1 + 33 + 6 + 1 + 1 + 1 + 19 = 64__x86_64__
: 1 + 1 + 1 + 44 + 6 + 1 + 1 + 1 + 8 = 64
下面给出isa
图解:
关联 isa
现在我们来看一下关联 isa
的代码实现:
inline void |
代码稍微精简了一下。注意这里的 SUPPORT_INDEXED_ISA
宏:
查阅到 资料,__ARM_ARCH_7K__ >= 2
应该是表示手表的宏。
__ARM_ARCH_7K__
架构上的 isa
的位域结构体具体的字段有所不同,分布也不同,这里就不在展开了。
在 isa
的实现代码里,一般继承 NSObject
的类都支持 isa
指针优化,如果不支持,isa
的 64
位都用来保存 class
或者 metaclass
的内存地址。
支持指针优化的 isa
:
isa
作为一个联合体,对bits
赋值后,isa
的值为(程序运行在模拟器,所以是基于__x86_64__
的架构):newisa.bits = ISA_MAGIC_VALUE;
/**
* 0x 0b
* 0x001d800000000001ULL 0b0000000000011101100000000000000000000000000000000000000000000001
*/此时位域的最低位即
nonpointer
为1
,表示支持指针优化。hasCxxDtor
放在联合体位域的has_cxx_dtor
,此时此处指为false
,所以值为0
newisa.has_cxx_dtor = hasCxxDtor;
/**
* 0x 0b
* 0x001d800000000001ULL 0b0000000000011101100000000000000000000000000000000000000000000001
*/cls
的内存地址放在isa
的shiftcls
区间newisa.shiftcls = (uintptr_t)cls >> 3; // 此处 cls 的值为 0x00000001000029f0
/**
* 0x 0b
* 0x001d8001000029f1 0b0000000000011101100000000000000100000000000000000010100111110001
*/注意此时的
shiftcls
对cls
的地址进行了右移3
位的计算,所以后面再去的时候,也是需要计算的。或许在这里你会有个疑问,地址经过计算之后,存储的地址不会发现变化吗?
这就是实现精妙的地方了 – 还记得上面讲的字节对齐吗,OC 对象的内存地址首先进行了
8
字节的对齐,那么对象的内存地址肯定是8
的倍数。虽然针对的是对象,但是类和元类在编译时创建同样也是经过了8
字节对齐的。所以内存地址也是8
的倍数。8
的 二进制表示为0b1000
,右移3位
之后位0b1
,所以后面再取值的时候,在左移3位
补齐后面的0
就能得到真正的地址。我们二进制计算一下:
(lldb) p/t 0x00000001000029f0
(long) $0 = 0b0000000000000000000000000000000100000000000000000010100111110000
(lldb) p/t $0 >> 3
(long) $1 = 0b0000000000000000000000000000000000100000000000000000010100111110
(lldb) p/t $1 << 3
(long) $2 = 0b0000000000000000000000000000000100000000000000000010100111110000
(lldb) po $0 == $2
true现在
cls
的地址已经放到isa
的shiftcls
段里面了,在arm64
里面占了33
位,但是cls
的地址是64
位能存下吗?类和元类只需要创建一个,而且是在编译时期就已经完成了的(我们可以从
machO
文件中看到这些类信息,所以他们在编译时期就已经确定了)。但是从实际问题出发,真的需要这么多的类吗?shiftcls
在arm64
架构下有33
位,加上右移的3
位,所以分配的内存空间为2^34 bit = 2 * 2^33 G = 2G
,我们的程序类信息(代码段)一般都不会这么大(我们打包完的程序还包含其他一下资源文件),所以是完全足够的。接下来就是如何将
cls
的地址如何从shiftcls
中取出来了。上面的分析都是依据arm64
的,下面的我们从程序中跑一下,犹如我们是在模拟器上运行的,shiftcls
取的44
位。取出
cls
地址的方法有两种:利用掩码
ISA_MASK
进行 与运算:define ISA_MASK 0x00007ffffffffff8ULL
(lldb) p/t 0x00007ffffffffff8ULL
(unsigned long long) $0 = 0b0000000000000000011111111111111111111111111111111111111111111000也就是取
64
后面的47
位,也就是cls
的值。位运算:
shiftcls
存在3~46
位上,所以可以做如下运算:也就是取中间的
44
的值,然后在后面补3
个0
之后就是cls
的地址了。
cls
的地址已经存放到 isa
里面了,后面我们就可通过 isa
找到类,然后进行方法的调用等动作了。
isa 的走位
是时候来一张经典的 isa
走位图了:
这张图怎么理解呢?我们先来看一个例子:
在 OC 的继承链中,万物皆对象,所以他们都有 isa
指针,而 isa
的存储的值,就是上面走位图中虚线的走向:
- 对象的
isa
-> 类 - 类的
isa
-> 元类 - 元类的
isa
-> 根元类 - 根元类的
isa
-> 根元类
其中比较特殊的点在于 NSObject
,NSObject
因为是 OC 对象的根类的原因:
NSObject
对象的isa
指向NSObject
类NSObject
类的isa
指向NSObject
元类NSObject
元类的isa
指向自己,即根元类的isa
指向自己
上面的图中还要另外一条线,即 superclass
的继承链:
- 类的继承,
superclass
指向父类 - 元类的继承,
superclass
指向父元类 - 根类
NSObject
的superclass
指向 nil - 根元类的
superclass
指向根类
根据这个有个很有意思的面试题:
这里考察的就是对 isa
的理解:
首先我们看下 isKindOfClass
和 isMemberOfClass
的源码:
+ (BOOL)isKindOfClass:(Class)cls { |
+ (BOOL)isMemberOfClass:(Class)cls { |
这两个方法的实现都不复杂,isKindOfClass
里面有一个循环,会通过 superclass
一直找到 NSObject
。
上面的面试题中:
re1, re2, re3, re4
考察的是类与元类,根元类与根类之间的关系re5, re6, re7, re8
考察的是对象与类之间的关系