上篇文章我们探究发现结构体内成员顺序对结构体的内存分配大小产生了影响,接下来我们探究一下对象的内存分布
对象的内存分布与影响因素
首先为我们的类MyClass添加以下几个属性
打印一下p对象的内存分配的大小发现系统为其分配了48个字节
上篇文章我们分析得知对象内存储的是isa指针 + 成员变量的值,也就是说我们即使为类添加一些方法也不会对类创建的对象的内存分配大小产生影响,验证如下图
既然对象内存储了成员变量的值,那么对象的成员变量的值存储在我们对象里面的时候是按照对象的属性赋值的顺序存储的还是按照对象的属性的书写顺序来存储的呢?我们来研究一下,通过lldb指令调试(常用调试指令)打印6个8字节的p对象的内存地址
我们知道第一个8字节内存地址存储的是对象的isa指针,也就是从第二个8字节地址开始才是存储对象的成员变量的值,分别打印一下
通过打印结果我们发现第二个地址打印的值并不是我们之前赋值的成员变量的值,而age和number这两个成员变量的值并没有发现。仔细观察第二个地址值我们发现是不是可以拆分成两段去打印呢?打印试一下
我们发现age和number的值出来了,而age和number放在了同一个8字节内存地址内。至此我们发现对象的成员变量的值存储在我们对象里面的时候被自动重排了顺序,既不是按照属性赋值的顺序也不是按照属性书写的顺序来存储的。那么系统这样做的目的,我们也能够想到是为了优化内存。对象的内部计算需要的内存是用8字节对齐的,int类型的age占用4个字节, short类型的number占用2个字节,用同一个8字节内存存储也是可以做到的,那么同样我们如果再加一个char类型的属性a,a和age以及number总共占用1+4+2 = 7 个字节,并没有超出8字节,所以a、age、number应该也是存储在同一个8字节内存地址里,打印结果如下图所示
现在我们知道了系统在内存里会对我们对象的属性自动生成的成员变量值自动重排顺序,那么我们再看一下自己声明的成员变量和继承的属性会不会也会重排顺序呢
从打印结果来看,我们自己声明的成员变量并不会被自动重排顺序,并且在内存中的分布是按照成员变量声明的顺序存储的
从打印结果可以看出,父类自动生成的成员变量并没有和子类自动生成的成员变量一起被系统自动重排顺序,而是各自进行重排。这里出现这样的原因是当子类在继承父类的数据结构时,父类是一块连续的内存空间,子类是没有办法去修改父类的数据结构的,也就是说系统在进行属性重排的时候只是基于某一个类,并不会把子类的成员变量和父类的成员变量重排在一起。
联合体和位域
接下来我们再看一下联合体和位域,下图有两种结构体写法,我们分别打印一下两种结构体的大小
我们发现struct2的结构体大小只占1个字节,struct2的这种写法就是我们所说的位域。也是可以节省内存空间
关于位域:
1、类型说明符 位域名 : 位域长度,这种写法位域长度就表示用几个bit位存储该成员变量
2、位域的长度不能超过数据类型的最大长度。
例如:char类型成员变量最大只占8 bit,那么用位域标识最大不能超过8 bit
3、一个位域是存储在同一个字节当中的,如果这一个字节所剩的空间不够去存放另一个位域的时候,
另一个位域就会从下一个字节开始存放
位域我们了解了之后,再看一下联合体。
上图中t1为结构体, t2、t3为联合体。
我们逐个运行,在每个断点处都打印对应的结构体t1或联合体t2发现,结构体的成员变量赋值前后都可以看得到对应的值,而联合体在为每个成员赋值后,对于类型不同的其他成员的值都是错误或者无效的值,而类型相同的成员存储的值是相同的。
我们再打印一下t2和t2成员的内存地址发现是同一块内存空间,那么也就是说对于联合体:
1、联合体的成员共用一个内存空间,一次只能使用一个成员
2、联合体可以定义多个成员,访问不同的成员都是访问这个联合体的不同途径
3、对某一个成员赋值,会覆盖其他成员的值
4、节省一定的内存空间
我们在打印一下t2,t3的内存大小
此时我们发现联合体对于内存的分配:
1、联合体必须能够容纳最大的成员变量
2、通过第1点计算出来的大小必须是其最大成员变量(基本数据类型)的整数倍
分析到此,再结合上篇我们分析的结构体发现,结构体和联合体的区别:
1、联合体的成员变量之间互斥(不能共存);结构体成员变量可以共存。
2、联合体内存使⽤更为精细灵活,也节省了内存空间;结构体的内存开辟比较粗放。
nonPointerIsa
我们了解了联合体和位域之后,我们再看一下上篇文章所说的_class_createInstanceFromZone方法内的initInstanceIsa方法,这个方法是把创建的对象通过对象内的isa指针来关联到相应的类,isa指针内包含了对象所属的类对象的内存地址
进入initInstanceIsa方法内,我们发现其内部也是调用了initIsa方法
进入initIsa方法内,我们发现了其内部就是对对象的isa指针进行初始化,同时我们发现了isa_t的数据类型
进入isa_t我们发现,isa_t就是一个联合体,目的是为了兼容以前的版本,现在系统使用的isa是nonPointerIsa, nonPointerIsa概念的出现目的也是为了节省内存空间,是系统内存优化的一个手段。因为对象的isa是一个8字节的Class类型的结构体指针,主要是用来存储对象所属类对象的内存地址的,而存储类对象的内存地址不需要使用8字节这么大的内存空间,所以系统就把一些与对象息息相关的信息也存储到isa的内存空间内,而nonPointerIsa的信息都存储在ISA_BITFIELD这样一个结构体内
进入ISA_BITFIELD内,我们发现对于不同架构下的isa内存储的成员变量占用的位域长度也是不同的
nonPointerIsa内存储的信息以及及如何利用isa的位运算得到类对象
从上图我们可以看到isa内存储的类对象的地址空间在不同架构下所占用的位域长度分别为52、33、44
我们在objc源码的main文件里创建一个p对象。因为我使用的Mac电脑是Intelx芯片,运行走的是x86_64的架构。就以x86_64架构为例,计算一下isa内存储的类对象的内存地址。因为x86_64架构下isa存储类对象地址占用的位域长度是中间的44位,低3位和高17位存储的是其他信息,那么我们对isa指针的地址做 >> 3 << 20 >> 17 的位运算操作把低3位和高17位的位置数据清零就可以得到存储在中间44位的类对象的内存地址,验证一下
通过上图打印结果我们发现通过对p对象isa指针做位运算得到的内存地址就是我们类对象的内存地址,同样对于真机来说可以通过对isa指针地址做 >> 3 << 31 >> 28 的位运算来得到存储在中间33位的类对象的内存地址。其实我们还可以通过另一种方式直接得到我们的类对象地址值,通过相应系统架构下的ISA_MASK掩码 & 对象的isa指针就可以得到对象的类对象的内存地址
同样我们也可以通过对应的位运算得到存储在isa中的其他相关信息。用表示对象的引用计数值的 extra_rc 来举例(不同架构下extra_rc占用的位域长度不同,所以extra_rc的最大值也是不同的,在x86_64的架构下最大值就为2^8 - 1),在x86_64架构下,extra_rc用8 bit来存储。通过对isa指针地址进行 >> 56 的位运算就可以得到对象的引用计数值
以下列举了nonPointerIsa中各个字段存储的信息内容
以上我们探索了对象的内存分布及影响因素、联合体和位域、nonPointerIsa概念和nonPointerIsa内存储的信息以及如何利用isa的位运算得到类对象。至此我们对iOS对象的底层探索暂时告一段落