《iOS内卷》:NonPointer ISA

1,136 阅读6分钟

image.png

一、前言

我们都知道每个OC对象都包含isa指针,这个指针指向它的类对象。

在第一节我们介绍alloc的基本流程,介绍_class_createInstanceFromZone函数时,有这么一段代码:

image.png

可以看到,通过malloc函数申请到内存空间之后,obj只是单纯的一个内存指针,指向一块申请好的内存空间,还没有和类绑定在一起,还不能称为真正的isa。那么它后面是如何与类绑定在一起的呢?

答案在于initIsa这个方法。这个方法将obj和对应的类绑定起来,从而变成我们熟悉的isa

二、探索 initIsa()

点击initIsa查看方法的实现:

image.png

可以看到,isaisa_t类型。

1、isa_t

image.png

isa_t是一个union共用体,拥有两个成员:bitscls。其中clsprivate修饰的,只能通过get方法和set方法访问。

void setClass(Class cls, objc_object *obj);
Class getClass(bool authenticated);

共同体的特点大家都知道,成员间的值不能同时存在,只能单独存在、互相覆盖,也就是说bitscls只能同时存在一个。cls比较好理解,可以用来设置isa对应的类信息。

bits的作用是什么呢?

2、ISA_BITFIELD

顺着initIsa的源码往下看,可以看到这个代码:

struct {
    ISA_BITFIELD;  // defined in isa.h
};

可以猜测bits是用来支持位域操作的。

点击查看ISA_BITFIELD里对位域的定义。目前使用的是Intel的Mac电脑,所以以__x86_64__为例:

image.png

shiftcls:存储类指针的值。开启指针优化的情况下,在__arm64__架构中有33位⽤来存储类指针,而在__x86_64__中有44位。

magic:⽤于调试器判断当前对象是真的对象还是没有初始化的空间。

weakly_referenced:指对象是否被指向或者曾经指向⼀个ARC的弱变量,没有弱引⽤的对象可以更快释放。

deallocating:标志对象是否正在释放内存。

has_sidetable_rc:当对象引⽤计数⼤于10时,则需要借⽤该变量存储进位。

extra_rc:对象的引⽤计数值,是实际的引⽤计数值减1。例如,如果对象的引⽤计数为 10,那么extra_rc为 9。如果引⽤计数⼤于10, 则需要使⽤到上⾯的has_sidetable_rc

看到这里就明确了,bits用来支持位域操作,用来存储对象使用过程中的其他信息

3、non pointer isa

isa不仅仅是一个指针,它还可以包含对象使用过程中的其他信息。为什么要这么做呢?

我们知道,每个对象都拥有isa,占用8个字节的空间。我们以二进制的格式打印pimage.png

发现p的好多位置都是0,这些位置都是没有使用到的。对于这些位置,我们完全可以用来存储其他东西,例如:引用计数、对象是否正在释放、是否有指向一个弱引用变量......没错,这就是non pointer isa所做的事情。

non pointer isa:
isa除了包含指向类对象的信息,还包含使用过程的其他信息。

raw pointer isa:
isa仅包含指向类对象的信息。

在上面的源码中,对于raw pointer isa,会直接将类对象和obj指针绑定

newisa.setClass(cls, this);

而对于non pointer isa来说,就相对复杂一点了, 除了需要设置类信息,还需要设置其他的一些位域信息:

#if SUPPORT_INDEXED_ISA //手表⌚️平台
        ASSERT(cls->classArrayIndex() > 0);
        newisa.bits = ISA_INDEX_MAGIC_VALUE;
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
        newisa.bits = ISA_MAGIC_VALUE;
    #if ISA_HAS_CXX_DTOR_BIT
        newisa.has_cxx_dtor = hasCxxDtor;
    #endif
        newisa.setClass(cls, this);
#endif
        newisa.extra_rc = 1;

这里额外说一下SUPPORT_INDEXED_ISA

这个宏不管是在MacOS平台还是iOS平台,值都是0。看了一下相关资料,发现这是WatchOS下才用到的。这个宏的意思是:是否支持“将类存储在 isa 字段中作为类表的索引”。貌似,watch上由系统维护了一个类表,cls都指向表里的偏移位置。

WatchOS平台的知识我没有探索过,所以这个知识点仅供参考。

三、指针是如何成为isa的

那么,类对象信息是如何和一个指针绑定的呢?关注setClass方法:

image.png

shiftcls = (uintptr_t)newCls >> 3;是最核心的方法,它将类对象的地址存储到isashiftcls位域中。

这里有一个很讲究的操作:

为什么要向右平移三位?

我们从ISA_BITFIELD的定义中可以知道,__arm64__下的最大寻址空间MACH_VM_MAX_ADDRESS0x1000000000,一共是36位。__arm64__下的最大寻址空间MACH_VM_MAX_ADDRESS0x7fffffe00000,一共是47位。

但是这36位和47位我们需要都用掉吗?

我们知道,对象的内存占用都是8字节对齐的,所以对象存放的地址,肯定也是8字节对齐的,8字节对齐的话,地址的低三位都是0。既然都是0,那么我们就没必要把它显示出来了。这样shiftcls的位占用就少了3位。

通过右移三位的操作,减少了需要占用的内存空间。不得不说,苹果公司在优化这块是相当讲究👏。

四、从isa推导类的地址

经过前面的源码分析,我们知道了类对象是如何绑定到isa中的。现在,我们反过来,从使用者的角度,探索如何从isa推导出类的地址:

看下面这个栗子🌰: image.png

image.png

Person类对象的地址:0x00000001000085e0
isa指针的值:0x011d8001000085e1

按照我们的理解:isa指向类对象,所以isa的值应该和Person类对象的地址保持一致,但是打印出来的两个值明显不一致。为什么?

1、通过ISA_MASK获取

isa的值必须&ISA_MASK的才能得到真正的类对象地址。

#   define ISA_MASK        0x00007ffffffffff8ULL

不得不说将MASK翻译成面具真的非常非常贴切👍🏻。鼻子面具戴上去就会露出鼻子,眼睛面具戴上去就会露出眼睛,同理,ISA面具ISA_MASK)戴上去就会露出ISA。反观把MASK翻译成掩码,不知晦涩难懂多少倍。

image.png 结果和刚才得到的Person类对象地址完全一致。

2、通过位运算获取

仔细观察ISA_BITFIELD,可以发现,shiftcls前面有3位,后面有17位。 image.png

我们完全可以通过位运算,先 >>3位,移除右边3位的内容,然后 <<20位,移除左边17位的内容,再>>17位恢复shiftcls的位置,这样isa指针中剩下东西就是shiftcls了,也就是类对象的地址了。

image.png

五、总结

本节通过源码分析了non pointer isa的原理以及如何从isa推导出类对象的地址。从现在开始,聊到isa,我们能讲的不再只有“实例对象的isa指向类对象“,不是吗?😊