ObjC-Runtime源码注疏(一)

前言

本文注疏的源码版本为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


isa.jpg

上图展示了在不同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的源码注疏。

你可能感兴趣的:(ObjC-Runtime源码注疏(一))