前言
本文注疏的源码版本为objc-781版本
(一)从提问开始
拿到源码后,也不知从何入手。后来想了想,就从一道我们常见的面试题开始吧。isa指针是个啥?
从代码中,我们可以看到如下代码
#include "isa.h"
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
首先,isa_t就是isa指针的真正类型,它是一个联合体(union)而不是一个结构体(struct)。那么这里的第一问就来了,联合体与结构体啥区别?
union与struct相比,实际上更省内存,但它的“省内存”是靠牺牲所有字段的独立存储空间换来的。换句话说,结构体分配内存后,所有的字段都有独立的存储空间。而联合体是所有字段公用一个存储空间。
换句话说,当我们为cls字段赋值后,bits字段的值就不存在了。反之亦然。总之,union在内存分配时会根据自身内部所有的字段大小来进行比较,最终union会按照内存占用最大的字段的大小作为分配的依据。
那么在这里使用联合体,说明对于后面的信息传递而言,isa_t里的所有字段,只要有一个字段有效,就可以足够进行信息传递了。
(二)内观isa
下面咱们再看看字段。整个isa_t中有多个字段,分别是Class cls字段,uintptr_t bits字段以及一个宏定义的匿名结构体。下面我们逐个看一下:
Class cls 字段
Class类型实际上也是一个结构体,在objc-private.h中有以下定义,足以说明问题
struct objc_class;
struct objc_object;
typedef struct objc_class *Class;
typedef struct objc_object *id;
可以看到,Class实际上就是objc_class的结构体指针,而下面那句不就是常常出现的面试考题之一吗 —— “OC中id类型的实质”。从上面的定义也可以看到了,id实际上就是objc_object结构体的指针。
对于这两个结构体,后面会有详细的阐述,此处先跳过。所以说Class cls字段实际上就是指向objc_class的指针,其描述了当前对象指向的类型。
uintptr bits 字段
对于这个字段,我们要结合其下面的定义一起看,也就是如下代码:
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
这里看到,在bits字段下还有一套匿名的结构体,其具体信息定义在isa.h中。我们再来具体看一下:
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_BITFIELD \
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)
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 8
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
# else
# error unknown architecture for packed isa
# endif
可以看到,ISA_BITFIELD宏定义了再arm64下和x86下分配位数不一样的一组信息。结合这些信息,以及后面会说到的objc_object对isa进行初始化时的代码,我们大致对bits有了如下说明
首先bits就是以上所有信息的存储单元。换句话说,虽然bits只是一个uint_ptr类型的值,但实际上这个值本身可以通过不同位存储了不同的信息。从以上掩码中可以看出,shiftcls字段在不同的CPU架构中,定义了不同的长度。这其实就是存储当前对象Class地址的地方。
所以,bits说白了就是一个掩码值。OC语言的设计者将其按不同的位数,分别赋予了不同的信息存储。这样,一个值就可以存储很多信息,大大节省了空间。
匿名struct字段
该字段实际上在bits里面已经有所阐述,他就是一个对bits内部具体存储的信息的分段显示。这里就不再赘述了。这里我们倒是可以好好的聊一聊上面的那些宏定义。
首先,这里要先讲解一个概念,即“位域”的概念。以上面的代码为例
uintptr_t nonpointer : 1;
这句代码就定义了一个“位域”,它的实际意义是,在uintptr_t这个64位类型中,nonpointer字段占一位。那么其后的所有定义都是按顺序占用不同的“位数”,即has_assoc占1位、has_cxx_dtor占1位、shiftcls占44位等等。那么这些信息定义完成后,其总长度必然是一个uintptr_t的长度,也就是64
上图展示了在不同CPU架构下的isa内,bits值的各个字段分配的长度。我们依次说明一下:
nonpointer字段 占1位
该字段只占一位,只有0/1之分。其含义是标识当前isa是否是一个nonpointer。如果是0,则代表当前是一个纯粹的isa指针;如果是1,则代表当前isa内存储了其他信息,而其本身指向的cls的地址需要从shiftcls字段读取。
has_assoc字段 占1位
该字段只占一位,只有0/1之分。其含义是标识当前拥有该isa指针的对象(objc_object,下同)是否有关联对象。1代表有,0代表没有。如果是0的话,不会执行有关关联对象的操作。
has_cxx_dtor字段 占1位
该字段只占一位,只有0/1之分。其含义是标识当前拥有该isa指针的对象是否有C++或ObjC的析构函数。1代表有,则对象在释放时要执行析构逻辑;0代表没有,则释放时会更快一些。
shiftcls字段 x86占44位,arm64占33位
该字段在不同架构下有不同的位数,但都是在isa联合体中占位最多的字段,该字段实际上存储的是一个地址。该地址指向当前拥有isa的对象的class-object。掩码ISA_MASK就是获取这段地址。
magic字段 占位6位
该字段用来帮助调试器来判断当前拥有该isa的对象,是一个真实的对象,还是一个没有被初始化的空间。
weakly_referenced字段 占1位
该字段只占一位,只有0/1之分。其含义是标识当前对象是否有或曾经有过ARC的弱引用对象。1代表有;0代表没有。如果没有的话,对象在释放时不会执行有关弱引用释放相关的步骤,因此,释放时更快。
deallocating字段
该字段只占一位,只有0/1之分。该字段就是为了标识出当前对象是否处于“正在释放”的阶段。
has_sidetable_rc字段 占1位
该字段只占一位,只有0/1之分。该字段是为了标记是否用到了sidetable来记录引用计数。当对象引用计数大于 10 时,则需要借用该变量存储进位
extra_rc字段
记录引用次数,当表示该对象的引用计数值,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10, 则需要使用到上面的 has_sidetable_rc。
以上就是对isa_t的所有字段信息的解释。这里可以注意到,在arm64和x86中,shiftcls的位数相差较大。从其注释中也可以发现
//arm64
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/
//x86
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/
在不同的CPU架构下,留给shiftcls的最大虚拟寻址空间时不一样的。我们至少可以猜测,arm架构下,系统要占领的虚拟空间相较于x86会更多
一些,导致shiftcls可用的寻址空间被大幅压缩。
总结
至此,我们已经完全展示了isa指针或者说isa_t联合体的全貌,这部分的东西将直接影响到后面objc_object和objc_class的一些理解。后续,我们会持续为objc_object和objc_class的源码注疏。