前言
前面在 iOS 对象底层原理之alloc分析 中介绍了创建一个对象关键三步:
- 计算类占用的内存大小
- 根据计算出来的类占用的内存大小
size,对于size进行16进制对齐,并在堆中开辟空间 - 类(
isa)和创建出来的对象进行关联
前面两节已经介绍了计算类的内存大小和开辟内存空间,今天来分析一下isa 以及isa怎么和创建出来的对象进行绑定的
结构体和联合体
在探索isa之前,先来了解一下共同体和位域,因为isa就是通过共同体 + 位域实现的。
- 结构体 开发中结构体算是比较常用的了,使用如下:
struct DXJPersonStatus {
BOOL sleep;
BOOL eat;
BOOL play;
BOOL study;
}pStatus; // 人的状态
NSLog(@"sizeof(pStatus) = %lu",sizeof(pStatus));
最终打印: sizeof(pStatus) = 4,因为BOOL 占1个字节,4个变量,所以占用4个字节,那就是 4 * 8 = 32位。这里其实存在两个问题,第一个:BOOL用 0 或者 1 就可以表达,但是这里用4个字节(32位)表示;第二个:一个人不能又睡又吃又玩又学习的,睡 吃 玩 学习 只能有一种状态,从这个两个层面上考虑,这个设计是浪费了内存空间的。解决第一个问题,我们使用:结构体 + 位域解决。解决第二个问题,我们使用联合体 + 位域
- 结构体 + 位域
struct DXJPersonStatus {
BOOL sleep : 1; // 1表示sleep只占用1位,eat play study同理
BOOL eat : 1;
BOOL play : 1;
BOOL study : 1;
}pStatus; // 人的状态
NSLog(@"sizeof(pStatus) = %lu",sizeof(pStatus));
最终打印: sizeof(pStatus) = 1, 因为sleep eat play study分别只占用1位,总共4位,所以只占用了1个字节。相比上面的单纯的结构体节省了3倍的内存空间。
空间上确实节省了,可是睡 吃 玩 学习依旧是同时存在的,不符合常理,如下图所示,这里使用:共同体(联合体) + 位域
- 共同体 + 位域,这里丰富了一下内部的数据类型,便于探索不同的数据类型在结构体和联合体中的不一致,其中
DXJPersonStruct和DXJPersonUnion内部数据是一模一样,对于数据的赋值也是一模一样,接下来看下他们的表现。
struct DXJPersonStruct {
char *name;
int age;
double height ;
};
union DXJPersonUnion {
char *name;
int age;
double height ;
};
分析上图:
-
sizeof(pStruct),其中pStruct是一个结构体,char *占8个字节,int占4个字节,double占8个字节,总共占用了[0-7],[8-11],(12-15)[16-23] => 24字节 -
sizeof(pUnion),其中pUnion是一个联合体,联合体的大小取决于体内最大成员变量的大小,所以是8字节 -
红框打印中发现,结构体
pStruct内部数据都正常的显示了出来:name = "DXJ", age = 18, height = 163 -
联合体:
4.1 标签1:
pUnion只是创建,没有赋值时,内部数据都是指向的脏内存4.2 标签2:
pUnion,给name赋值,age和height依旧指向脏内存4.3 标签3:
pUnion,给age赋值,发现name = "",height依旧指向了脏内存4.4 标签4:
pUnion,给height赋值,发现name = "" age = 0
在探索过程中我们发现,联合体给内部成员变量赋值时,只保留当前成员变量的赋值,之前对于其他成员变量的赋值都置为了默认值,这里得出了联合体和结构体的特点:
联合体: 各个成员变量之间是互斥的,那缺点就是不够包容,优点是内存使用更为精细灵活,也节省了内存空间。
结构体: 各个成员变量之间是共存的,缺点就是无论变量是否使用都会开辟内存空间,优点是各个成员变量之间是共存。
isa
isa是做什么?在哪里有体现呢? 那首先查看下p这个对象在内存中分布:
对象的isa就是打印对象时,放在首位位置的内存地址,在libobjc.A.dylib库objc-private中找到了isa_t源码(截取主要部分):
union isa_t {
// 联合体的两种析构方法
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
/**
因为isa_t是联合体,所以bits 和 cls 以及 下方的结构体 是互斥的
cls 是private类型的,所以外部只能操作bits和下方的结构体
*/
uintptr_t bits;
private:
Class cls;
public:
// __x86_64__ 配置
// 改结构体内部是共存的
struct {
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 unused : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 8
};
// obj 和 cls 绑定
void setClass(Class cls, objc_object *obj);
// 获取类
Class getClass(bool authenticated);
// 获取编码后的类
Class getDecodedClass(bool authenticated);
};
分析一下上方代码,主要考虑成员变量:因为isa_t是一个联合体,所以成员变量bits 和 cls 以及结构体(结构体内部是共存的) 他们三个是互斥的,也就是
bits被赋值时,cls如果曾经有赋值则被bits的值覆盖,如果未曾赋值则为空cls被赋值时,bits如果曾经有赋值则被cls的值覆盖,如果未曾赋值则为空
分析结构体内部数据之前,先看下这些成员变量表达的含义,这是结构体+位域组成的,占8个字节 1 + 1 + 1 + 44 + 6 + 1 + 1+ 1 + 8 = 64 => 8字节,这里也体现了位域使内存更加精细灵活。以x86_64(模拟器)做分析:
nonpointer : 1: 占用1bit,表示是否对isa做了指针优化,1:不止类的地址,还包含了其他的信息,例如 对象的引用计数,是否有关联对象,析构函数,是否指向ARC的弱引用变量.....has_assoc : 1:占用1bit,关联对象标志位,1:有,0:没有has_cxx_dtor : 1:占用1bit,该对象是否有C++或Objc的析构函数,1:有,0:没有shiftcls : 44:占用44bit,类的指针的值magic : 6:占用6bit,用于调试器判断当前对象是真的对象还是么有初始化的空间weakly_referenced : 1:占用1bit,对象是否被指向或曾经指向一个ARC的弱变量,没有弱引用的对象可以更快释放extra_rc : 8:占用8bit,该对象的引用计数值,实际上是引用计数值减1- ...
isa底层实现总结
- isa有两种类型
nonpointer和 非nonpointer类型,nonpointer包含的类其他信息,是否有关联对象,是否有析构函数,对象引用计数等;非nonpointer是一个纯类的指针 - 在开发中,会频繁的创建对象,每个对象有包含
isa指针,这样会占用大量的内存,所以isa采用了联合体+位域的算法来实现,联合体中成员变量互斥的特性,节省了部分内存空间,位域的使用,优化了内存的存储,使得内存的使用更加灵活。这样isa占用的内存空间得到了很大的优化。
obj绑定isa
分析:
isa_t newisa(0)创建一个isa_t类型的实例,内部数据为空,如下图
-
如果是非
nonpointer,也就是没有开启指针优化,是一个纯的isa,直接将cls和this绑定newisa(cls, this)(这里的this就是调用者,也就是obj) -
如果是
nonpointer类型,说明开启了指针优化,包含了其他的信息3.1
newisa.bits = ISA_MAGIC_VALUE,此时除了bits会赋值,结构体内部的magic 和 no npointer也会赋值,magic是占用了[48 - 53]位,nonpointer占用了第1位,根据ISA_MAGIC_VALUE掩码,计算得出magic = 59, nonpointer = 13.2
ISA_HAS_CXX_DTOR_BIT在x86_64下为1,newisa.has_cx x_dtor = hasCxxDtor;是否有C++析构函数3.3
newisa.setClass(cls, this);然后再将cls和this绑定(这里的this就是调用者,也就是obj)
扩展 ISA_MASK
在isa_t中发现一个获取类内存地址的方法Class getClass(bool authenticated);其中有一个关键代码
uintptr_t clsbits = bits;
clsbits = clsbits & ISA_MASK;
// 也就是 bits = bits & ISA_MASK
ISA_MASK:isa的掩码,isa分别两种,其中nonpointer类型会包含一些其他的信息,有时候我们指向获取到shiftcls的信息,这里提供了两种方式_x86_64举例
0x011d800104179e6d,这是一个nonpointer类型的isa,怎么获取到shiftcls?
- 通过位移的方式获取
// 0. x86_64的isa数据分布:这里要获取的就是shiftcls
(magic + weakly_referenced + unused + has_sidetable_rc + extra_rc)+ shiftcls + (nonpointer + has_assoc + has_cxx_dtor) = 17 + 44(shiftcls) + 3
// 1. 进制转化
十六进制:
0 1 1 d 8 0 0 1 0 4 1 7 9 e 6 d
转二进制:
0000 0001 0001 1101 1000 0000 0000 0001 0000 0100 0001 0111 1001 1110 0110 1101
//2. 右移三位 最有右端的101移出去了,最左端补了三个零
000 0000 0001 0001 1101 1000 0000 0000 0001 0000 0100 0001 0111 1001 1110 0110 1
//3. 左移20位 17 + 3
000 0000 0000 0001 0000 0100 0001 0111 1001 1110 0110 1000 0000 0000 0000 0000 0
//4. 右移17位,回归44原来的位置
0000 0000 0000 0000 0 000 0000 0000 0001 0000 0100 0001 0111 1001 1110 0110 1000
//5. 转化成16进制
0b 0000 0000 0000 0000 0 000 0000 0000 0001 0000 0100 0001 0111 1001 1110 0110 1000
0x 0 0 0 0 0 0 0 1 0 4 1 7 9 e 6 8
0x0000000104179e68 便是最终的shiftcls
- 通过
ISA_MASK掩码,按位与
// 1. 进制转化
# define ISA_MASK 0x00007ffffffffff8ULL // ULL 无符号长整型
十六进制:
0 0 0 0 7 f f f f f f f f f f 8
转二进制:
0000 0000 0000 0000 0111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1000
// 2.
十六进制:
0 1 1 d 8 0 0 1 0 4 1 7 9 e 6 d
转二进制:
0000 0001 0001 1101 1000 0000 0000 0001 0000 0100 0001 0111 1001 1110 0110 1101
// 3. 按位与
0000 0000 0000 0000 0111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1000
& 0000 0001 0001 1101 1000 0000 0000 0001 0000 0100 0001 0111 1001 1110 0110 1101
0000 0000 0000 0000 0000 0000 0000 0001 0000 0100 0001 0111 1001 1110 0110 1000 = 0x0000000104179e68
// 0x0000000104179e68 便是最终的shiftcls。
// 其实这里有更简单的计算方式,这里就不描述了(按照按位与的特性,同为真时才为真)