OC 内存对齐
- 影响oc对象内存大小的参数
我们来做一些实验, 需要用到APIclass_getInstanceSize
去获取实例对象的大小.
此时我们
MLPerson
里面没有任何成员变量和方法,那么为什么size会为8呢?那只能猜测继承自NSObject
里面的一些变量导致的吧,在此不做过多解释, 直接给答案:NSObjec
里面有一个成员变量isa
是一个指针,这是一个Class
类型. 那么我们知道指针的大小是8字节,所以MLPerson
的大小也是8字节
,是一个结构体指针,所以固定会先有 8 字节
增加一个指针成员变量
可以看到size变成了16了,得出第一个结论成员变量会影响实例变量的size.
再增加一个property
可以看到size又增加了, 变成24, 得出第二个结论,property 会影响实例变量的size.
那实例方法和类方法是否会影响实例变量的size呢?我们向MLPerson
中添加类方法sayHello
和实例方法sayHi
, 运行结果如下, 此时发现方法并不影响实例变量的size.
再次更改
MLPerson
,请注意@synthesize email = memail;
注释前和注释后对size变化的影响
注释后
注释前
当使用
synthesize
将成员变量memail
和email
绑定在一起,那么就说明两个变量共享同一片内存,所以@synthesize email = memail;
之后size为16.
我们知道property 默认会生成一个成员变量,因此我们用@dynamic
禁止 email 生成成员变量,再次观察结果如下:
由此观察发现,当属性
email
不再生成成员变量时,内存变为了16
。因此我们发现属性之所以能够影响内存,是因为生成了成员变量,也就是说最终影响对象内存大小的是成员变量.
此时我们是否就可以得出,实例变量的size就等于各个property和成员变量想加之和呢?
我们继续做实验,添加@property (nonatomic, assign)int age;
, 发现结果和我们预想的有出入
注释
age
int:4 字节. 应该是 8(isa) + 8(email) + 8(name) + 4(age) = 28 != 36
. 由此引入下一个话题, 内存的分布
- 内存的分布及优化
由上一小结可以知道, 一个对象的内存大小受其成员变量的影响,其内存也由成员变量组成,
MLPerson
的内存构成如下图:
person 指针指向一块内存区域,这块内存即是存储这个对象的地方,包括 isa 和 其他成员变量。那么问题来了,这些成员变量如何分布呢,是按顺序存储的吗?我们依然以MLPerson
为例进行探索。
类的结构如下:
然后使用 x/8gx person 来查看内存(x/4gx表示以16进制显示,8个字节一组),结果如下:
:
左边为内存地址,:
右边为存储的值, 0x101d1e0d0 为对象首地址,其后 0x001d80010000136d 为 isa
,之后 0x0000000000000012 为 age
,之后是0x0000000100001078 为email
,之后是0x0000000100001098 为 name
.
由此我们发现,内存的分布并非完全按照顺序排列,而是会做一定的优化,比如age
放到了isa
后面的第[5,8]字节,而email
和name
则是顺序存储的,这是系统帮我们做的优化,其目的是为了方便提高访问效率的同时,能够优化存储,减少浪费。下面我们就来探讨内存对齐的规则.
- 内存对齐的规则.
-
数据成员对⻬规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int为4字节,则要从4的整数倍地址开始存储。 min(当前开始的位置mn)m=9 n=4 9 10 11 12
-
v结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储.(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.)
-
收尾工作:结构体的总大小,也就是sizeof的结果,.必须是其内部最大成员的整数倍.不足的要补⻬。
根据以上规则是不是就解释了为什么
8(isa) + 8(email) + 8(name) + 4(age) = 28 != 36
的问题,因为我们需要以最大的变量占用的内存进行对齐,不足要补齐,如果是28, 那么28 不是8的倍数,我们需要补齐为8的倍数,因此,MLPerson
的size 为36.typedef struct { double a; // double:8字节 第一个成员 start = 0, 存储位置 [0 7] char b; // char:1字节 8是1的倍数 start = 8, 存储位置 [8 9 10 11] int c; // int:4字节 12是4的倍数 start = 12, 存储位置 [12 13 14 15] short d; // short:2字节 16是2的倍数 start = 16, 存储位置 [16 17] // [0 17] 共18个字节,但不是double大小,即8的倍数,最终大小为 24 } MLStruct1;
typedef struct { double a; // double:8字节 第一个成员 start = 0, 存储位置 [0 7] char b; // char:1字节 8是1的倍数 start = 8, 存储位置 [8 9 10 11] short c; // short:2字节 12是2的倍数 start = 12, 存储位置 [12 13] int d; // int:4字节 14、15均不是4的倍数,跳过,start = 16, 存储位置 [16 17 18 19] MLStruct1 str; // 最大成员为 8字节,20、21、22、23均不是倍数,跳过,start = 24 // 算下来大小为 24 + 16,共30字节,但需要是最大成员倍数,即 8 的倍数,最终结果为 32 字节 } MLStruct2;```
- 为什么要内存对齐
如果没有内存对齐,以MLStruct1
为例,内存存储如下
这样存储的结果是占用内存确实小了一些,但是在读取上却有几点不便:
- 每次读取都需要根据当前成员的大小计算要读取的空间大小,然后才能进行读取,这样无疑降低了读取效率
- 假定读取时设定一个尺度读取,如果以最大的a的大小为尺度,每次都读 8 字节,则当读取 b 时,读取的空间就超出了结构体的空间,容易发生错误读取
- 假定以其它的成员大小读取,会发生一次读取不完整的情况,例如以 char 的大小 1字节读取,则其它成员都无法读取完整。
总之不对齐时,读取上会有很大的不便。下面看下进行内存对齐的情况:
这种对齐后的方式,会找到最大的度量,然后以这个度量为尺度,每次读取这么大的长度,有以下优点:
- 以最大长度读取时,不会超出结构体的内存空间,因为总长度是最大长度的倍数
- 各个成员起始都以自身倍数开始,就保证了以最大尺度读取时,每次都可以读取完整数据,而读取次数相对于逐个读会大大减少
结构体对齐之后,虽然占用存储空间大了一些,但是在读取效率上会大大减小,例子中不对齐需要读取4次,每次还要重新计算要读取多少空间,采用对齐之后只需要读取两次,每次读取 8 字节即可,这就是一种以空间换时间的思想,由此也可以看出结构体内存对齐的意义所在。
malloc_size VS class_getInstanceSize
看下面这段代码,发现class_getInstanceSize 和malloc_size 取到的size是不一样的
class_getInstanceSize获取的是对象的大小,但是我们在生成
person
这个对象的时候,是在堆上申请内存的. 我们找到libsystem_malloc 的源码分析,找到了给一个关键的地方如下
calloc
调用去申请内存空间是以16字节对齐的, 那就解释了为什么instanceSize 是40 但是实际这个指针指向的内存空间是48 字节的.
此时引出一个问题,为什么对象之间的对齐是16字节?类里面的成员变量是以8字节对齐的,那么如果对象之间也是以8字节对齐,此时在堆上的内存全是紧挨着,那么内存访问错误的概率都会增大. 如果是以16字节对齐,内存都会有冗余,降低内存访问错误的概率. 此时可能会联想到一个问题,为啥不是32字节对齐,如果是16字节对齐,每个对象最多浪费8个字节,如果是32字节对齐,那就会最大浪费24字节, 得不偿失.
附录
p/f 打印浮点型
转载自:https://juejin.cn/post/6975810773813559303