在 alloc的初探 中了解了如何获取对象大小,内存对齐的原则,对象的 alloc
,但是在调用 calloc
在堆上开辟一个内存空间时返回了一个指针地址,这时候我们如何将这个指针地址和当前对象关联呢?
下方源码中 initInstanceIsa
就干的是这些事情。
//...
obj = (id)calloc(1, size);
if (!obj) return nil;
obj->initInstanceIsa(cls, hasCxxDtor);
//...
1、什么是 isa?
简单点来说,其实就是英文 is a
写到一起了,说明某一个对象是什么。比如: object is a NSObject
,但是 isa
经过发展之后存储的东西变得相当的庞大。
官方对 isa
的解释为:
每个对象都是通过 isa
实例变量连接到运行时系统,从 NSObject
类继承。Isa
标识对象的类;它指向一个结构的类定义编译。
通过 ISA
,可以在运行时找到一个对象的所有信息,如继承层次结构中的位置,它的实例变量的大小和结构,以及可以相应消息的方法所实现的位置。
2、isa 存储结构分析
进入 initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
函数之后发现,isa
是一个 isa_t
的类型。
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
};
# if __arm64__
# 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
# elif __x86_64__
# 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
上方代码可以看到 isa_t
类型是一个 union
联合体。ISA_BITFIELD
是位域。
1、什么联合体?
联合体又被称为共用体,顾名思义就是在 union
定义下的变量共用一块内存单元,赋值时相互覆盖,这种结构被称为联合体。
2、什么是位域?
有些信息在存储时,并不需要占用一个完整的字节, 而只需占几个或一个二进制位。例如在存放一个开关量时,只有 0 和 1 两种状态, 用一位二进位即可。
为了节省存储空间,并使处理简便,C语言又提供了一种数据结构,称为 位域 或 位段。
所谓 位域 是把一个字节中的二进位划分为几 个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。 这样就可以把几个不同的对象用一个字节的二进制位域来表示。
位段成员必须声明为 int
、unsigned int
或 signed int
类型(short char long
)。
3、位域如何定义?
位域列表定义的形式为: 类型说明符 位域名:位域长度
struct 位域结构名
{
位域列表
};
例如:
struct bits
{
int a:8;
int b:2;
int c:6;
};
说明 bits
,共占两个字节。其中位域 a
占 8
位,位域 b
占 2
位,位域 c
占 6
位。
4、isa_t 的位域
isa_t
中 bit
是 unsigned long
类型,占用 8
个字节,也就是 8byte = 64 bit
,所以在 __arm64__
和 __x86_64__
下 isa_t 的位域大小都是 64
个 bit
,只是因为架构不同所以占得位数不同罢了。
3、isa 的初始化
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
//...
if (!nonpointer) {
isa.cls = cls;
} else {
isa_t newisa(0);
newisa.bits = ISA_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.shiftcls = (uintptr_t)cls >> 3;
//...
isa = newisa;
}
}
在初始化 isa
的时候会将 alloc
出来的对象和类进行绑定 obj -> isa -> shiftcls
。
当执行 TestClass *object = [TestClass alloc];
创建一个对象的时候,object
的 isa
已经创建完毕了,并且将对象的类写入了 shiftcls
。
1、对 shiftcls 使用二进制验证
用 x/4gx
以16进制打印4个8字节的内存值,那么第一个一定是 isa
。
-
p/t
以二进制打印isa
的内存值0x001d800100001139
,获取到64个bit
的二进制。 -
p/x TestClass.class
打印TestClass
的类, 获取指针地址。 - 根据
(uintptr_t)cls >> 3
还原。
最后比对结果是一样的。
$2 = 0b0000000000000000000000000000000100000000000000000001000100111000
$7 = 0b0000000000000000000000000000000100000000000000000001000100111000
2、对 shiftcls 使用16进制蒙版验证
在 objc_class()
函数里就有对类的返回,使用的蒙版取 3~48 位。
inline Class
objc_object::ISA()
{
//...
//这里是返回类对象需要用 isa 的指针 & ISA_MASK
return (Class)(isa.bits & ISA_MASK);
}
验证如下:
4、类在内存中存在的个数
既然已经知道了对象的 isa
指向了类,那类到底是怎样的呢?对象在实例化的时候,每个对象都是不同的,其指针地址也是不相同的,那么类呢?能否在内存中存在多份?
下方验证一下:
Class class1 = [TestClass class];
Class class2 = [TestClass alloc].class;
Class class3 = object_getClass([TestClass alloc]);
Class class4 = [TestClass alloc].class;
NSLog(@"\n%p \n%p \n%p \n%p \n",class1,class2,class3,class4);
打印结果过下:
2019-12-22 15:33:01.539549+0800 objc-debug[3353:135796]
0x100001170
0x100001170
0x100001170
0x100001170
发现类对象的地址都是相同的,说明类对象在内存中有且只能存在一个。
5、isa 的指向分析
在对象返回前,会给 isa
进行赋值,标识对象属于什么类,对象指向的类其实也是一个对象,这种对象被称为类对象。
既然是对象那必定存在 isa
,那么类对象的 isa
又指向什么呢?
x/4gx
打印类的内存结构:
打印出来的内存结构,其实都是内存的值,真正指向当前对象的指针地址是最前面的 0x100001138
,po 0x100001138
发现这个对象竟然也是 TestClass
类型的,这个 TestClass
其实是指向类对象的类,又称为元类。
元类是系统创建的,当程序中有一个类被声明,在编译器编译时,会相应的生成一个指向类对象的元类,以便以保存类的一些相关信息,比如:类方法等。
我们知道了对象的 isa
指向类对象,类对象的 isa
指向元类,那么元类的 isa
指向什么呢?
接下来使用上方证明对象的 isa
指向的方法来推导一下元类的 isa
的指向。
推导过程如下:
从上图能看到 TestClass
的类对象的 isa
指向 TestClass
的元类。
继续查看 TestClass
元类的 isa
指向的是 NSObject
,那这个 NSObject
到底是元类还是类对象呢?
因为内存中类对象只有一个,类对象指针地址唯一,所以如果 NSObject.class
的指针地址和 TestClass
元类的 isa
指向地址相同则说明是 isa
指向 NSObject
类,否则不是。
继续查看 NSObject
类的内存地址, p/x NSObject.class = 0x0000000100b38140 NSObject
,很遗憾并不是 NSObject
的类对象。
查看 NSObject
类对象的元类,发现了 NSObject
元类的指针地址和 TestClass
元类 isa
指向的地址是相同的,这就说明了 TestClass
元类的 isa
指向的是 NSObject
元类。
继续查看 NSObject
元类 isa
的指向,发现指向的是自己。
所以就有了苹果老大给的下方的图。
6、isa 的指向补充
在上方的指向中,subclass
的 meta class
直接指向了 NSObject
的 meta class
,是因为元类保存的就是类的信息,可以说已经到头了, 如果再次继承 Super meta class
已经没有什么意义,并且还会让继承关系更加复杂,使得继承树更加难以维护,所以苹果将 subclass
的 meta class
直接指向了 NSObject
的 meta class
,再将 NSObject
的 meta class
指向了自己形成了一个闭环。
以上就是 isa
的初始化过程和指向分析。