isa指针
对象的isa指针
,用来表明对象所属的类的类型。
但是如果isa指针
仅表示类型的话,对内存显然也是一个极大的浪费。在arm64架构
之前,isa
就是一个普通的指针,存储着Class
或者Meta-Class
对象的内存地址。从arm64架构
开始,对isa进行了优化,变成了一个共用体(union)结构
,还使用位域
来存储更多的信息。
首先我们来看下isa指针是怎么在一个对象中存储的。看下面runtime简化的源码
:
struct objc_object {
private:
isa_t isa;
}
struct objc_class : objc_object {
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
}
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
# if __arm64__ // arm64下isa需要用的操作 `位掩码`
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33; // MACH_VM_MAX_ADDRESS 0x1000000000 保存的是对象的地址
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)
};
}
从代码中可以看出,我们所谓的isa指针
,最后实际上落脚于isa_t的共用体结构(也叫做联合类型)
。共用体结构
是C语言
中的一种类型,简单来说,就是一种n选1的关系。比如isa_t
中包含有cls,bits, struct
三个变量,它们的内存空间是重叠的。在实际使用时,仅能够使用它们中的一种,你把它当做cls
,就不能当bits
访问,你把它当bits
,就不能用cls
来访问。
补充
:
结构体和共用体的区别在于
:结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。
联合体
: 内部的变量公用同一个内存地址。这也就意味着当多个变量都进行赋值的时候,可能只有一个变量的地址有正确有效的。
结构体占用的内存大于等于所有成员占用的内存的总和(成员之间可能会存在缝隙),共用体占用的内存等于最长的成员占用的内存。
共用体使用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉。
联合的作用在于,用更少的空间,表示了更多的可能的类型,虽然这些类型是不能够共存的。
将注意力集中在isa_t共用体
上,我们该怎样理解它呢?
首先它有两个构造函数isa_t()
, isa_t(uintptr_value)
, 这两个定义很清晰,无需多言。
然后它有三个数据成员Class cls
, uintptr_t bits
, struct
。 其中uintptr_t
被定义为typedef unsigned long uintptr_t
,占据64位内存。
关于上面三个成员, uintptr_t bits
和 struct
其实是一个成员,它们都占据64位内存空间,之前已经说过,联合类型的成员内存空间是重叠的。在这里,由于uintptr_t bits
和 struct
都是占据64位内存,因此它们的内存空间是完全重叠的。而你将这块64位内存当做是uintptr_t bits
还是 struct
,则完全是逻辑上的区分,在内存空间上,其实是一个东西。
即uintptr_t bits
和 struct
是一个东西的两种表现形式。
uintptr_t bits
是用来使用 struct里是用来解释说明bits里的值分配和作用表示
struct
内的位域 增加可读性 对bits
解释说明 而说明的占位加起来不能超过外面的所代表的uintptr_t bits
变量的字节大小。
在struct
内的nonpointer、has_assoc、has_cxx_dtor
顺序没有任何意思,只是解释说在bits中什么位置占多少位表示什么意思
。
具体的所处的位置主要取决于在set、get
的时候与负责哪个区域的位掩码进行的位操作运算。一般都是按照从低位到高位的,这里也是如此
。
实际上在runtime中,任何对struct
的操作和获取某些值,如extra_rc
,实际上都是通过对uintptr_t bits
做位操作实现的。uintptr_t bits
和 struct
的关系可以看做,uintptr_t bits
向外提供了操作struct
的接口,而struct
本身则说明了uintptr_t bits
中各个二进制位的定义。
理解了uintptr_t bits
和 struct
关系后,则isa_t
其实可以看做有两个可能的取值,Class cls
或struct
。如下图所示:
当isa_t
作为Class cls
使用时,这符合了我们之前一贯的认知:isa是一个指向对象所属Class类型的指针。然而,仅让一个64位的指针表示一个类型,显然不划算。
因此,绝大多数情况下,苹果采用了优化的isa策略,即,isa_t
类型并不等同而Class cls
, 而是uintptr_t bits
。
-
uintptr_t bits
对于struct位域
中,struct
共占用64位,从低位到高位依次是nonpointer
到extra_rc
。成员后面的:表明了该成员占用几个bit。成员的含义如下:
成员 | 位 | 含义 |
---|---|---|
nonpointer | 1bit | 标志位。1(奇数)表示开启了isa优化,0(偶数)表示没有启用isa优化。所以,我们可以通过判断isa是否为奇数来判断对象是否启用了isa优化。 |
has_assoc | 1bit | 标志位。表明对象是否有关联对象。没有关联对象的对象释放的更快。 |
has_cxx_dtor | 1bit | 标志位。表明对象是否有C++或ARC析构函数。没有析构函数的对象释放的更快。 |
shiftcls | 33bit | 类指针的非零位。 |
magic | 6bit | 固定为0x1a,用于在调试时区分对象是否已经初始化。 |
weakly_referenced | 1bit | 标志位。用于表示该对象是否被别的对象弱引用。没有被弱引用的对象释放的更快。 |
deallocating | 1bit | 标志位。用于表示该对象是否正在被释放。 |
has_sidetable_rc | 1bit | 标志位。用于标识是否当前的引用计数过大,无法在isa中存储,而需要借用sidetable来存储。(这种情况大多不会发生) |
extra_rc | 19bit | 对象的引用计数减1。比如,一个object对象的引用计数为7,则此时extra_rc的值为6。 |
由上表可以看出,和对象引用计数相关的有两个成员:extra_rc
和has_sidetable_rc
。iOS用19位的extra_rc
来记录对象的引用次数,当extra_rc
不够用时,还会借助sidetable
来存储计数值,这时,has_sidetable_rc
会被标志为1。
我们可以算一下,对于19位的extra_rc
,其数值可以表示2^19 - 1 = 524287。 52万多,相信绝大多数情况下,都够用了。