一、前言
我们都知道每个OC对象都包含isa
指针,这个指针指向它的类对象。
在第一节我们介绍alloc
的基本流程,介绍_class_createInstanceFromZone
函数时,有这么一段代码:
可以看到,通过malloc
函数申请到内存空间之后,obj
只是单纯的一个内存指针,指向一块申请好的内存空间,还没有和类绑定在一起,还不能称为真正的isa
。那么它后面是如何与类绑定在一起的呢?
答案在于initIsa
这个方法。这个方法将obj
和对应的类绑定起来,从而变成我们熟悉的isa
。
二、探索 initIsa()
点击initIsa
查看方法的实现:
可以看到,isa
是isa_t
类型。
1、isa_t
isa_t
是一个union
共用体,拥有两个成员:bits
和cls
。其中cls
是private
修饰的,只能通过get
方法和set
方法访问。
void setClass(Class cls, objc_object *obj);
Class getClass(bool authenticated);
复制代码
共同体的特点大家都知道,成员间的值不能同时存在,只能单独存在、互相覆盖,也就是说bits
和cls
只能同时存在一个。cls
比较好理解,可以用来设置isa
对应的类信息。
bits的作用是什么呢?
2、ISA_BITFIELD
顺着initIsa
的源码往下看,可以看到这个代码:
struct {
ISA_BITFIELD; // defined in isa.h
};
复制代码
可以猜测bits
是用来支持位域操作的。
点击查看ISA_BITFIELD
里对位域的定义。目前使用的是Intel的Mac电脑,所以以__x86_64__
为例:
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
个字节的空间。我们以二进制的格式打印p
:
发现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
方法:
shiftcls = (uintptr_t)newCls >> 3;
是最核心的方法,它将类对象的地址存储到isa
的shiftcls
位域中。
这里有一个很讲究的操作:
为什么要向右平移三位?
我们从ISA_BITFIELD
的定义中可以知道,__arm64__
下的最大寻址空间MACH_VM_MAX_ADDRESS
是0x1000000000
,一共是36
位。__arm64__
下的最大寻址空间MACH_VM_MAX_ADDRESS
是0x7fffffe00000
,一共是47
位。
但是这36
位和47
位我们需要都用掉吗?
我们知道,对象的内存占用都是8
字节对齐的,所以对象存放的地址,肯定也是8
字节对齐的,8
字节对齐的话,地址的低三位都是0
。既然都是0
,那么我们就没必要把它显示出来了。这样shiftcls
的位占用就少了3
位。
通过右移三位的操作,减少了需要占用的内存空间。不得不说,苹果公司在优化这块是相当讲究👏。
四、从isa推导类的地址
经过前面的源码分析,我们知道了类对象是如何绑定到isa
中的。现在,我们反过来,从使用者的角度,探索如何从isa
推导出类的地址:
看下面这个栗子🌰:
Person
类对象的地址:0x00000001000085e0
isa
指针的值:0x011d8001000085e1
按照我们的理解:isa
指向类对象,所以isa
的值应该和Person
类对象的地址保持一致,但是打印出来的两个值明显不一致。为什么?
1、通过ISA_MASK获取
isa
的值必须&ISA_MASK
的才能得到真正的类对象地址。
# define ISA_MASK 0x00007ffffffffff8ULL
复制代码
不得不说将MASK
翻译成面具
真的非常非常贴切👍🏻。鼻子面具戴上去就会露出鼻子,眼睛面具戴上去就会露出眼睛,同理,ISA面具
(ISA_MASK
)戴上去就会露出ISA
。反观把MASK
翻译成掩码,不知晦涩难懂多少倍。
结果和刚才得到的
Person
类对象地址完全一致。
2、通过位运算获取
仔细观察ISA_BITFIELD
,可以发现,shiftcls
前面有3
位,后面有17
位。
我们完全可以通过位运算,先 >>3
位,移除右边3
位的内容,然后 <<20
位,移除左边17
位的内容,再>>17
位恢复shiftcls
的位置,这样isa
指针中剩下东西就是shiftcls
了,也就是类对象的地址了。
五、总结
本节通过源码分析了non pointer isa
的原理以及如何从isa
推导出类对象的地址。从现在开始,聊到isa
,我们能讲的不再只有“实例对象的isa指向类对象“,不是吗?😊