OC底层原理06:内存对齐探索
查看内存地址的方法
1、【方法一】:Debug->Debug Workflow->Alway View Memory
2、【方法二】:x 地址
/x 对象
1)下面我们通过代码具体查看一下使用:
1 | @interface ZJPerson : NSObject |
2)通过LLDB编译命令查看内存情况:
x 0x100867a60
:以16进制打印当前地址的内存情况,查看不方便x/4gx 0x100867a60
:以16进制形式打印4个地址,查看方便0x100867a60
: 内存地址0x001d80010000220d
:isa0x000000000000001e
:300x0000000100001010
:ZJ
3)那么 0x100867a60、0x102038770
和 0x001d80010000220d 0x000000000000001e 0x0000000100001010 0x0000000000000000
有什么关系呢?我们用一张图来表示一下:
4)如果有一个 double/float
类型的 height
呢?
我们来分析一下:
1 | @property (nonatomic, assign) double height; |
通过 x/4gx p
查看打印的结果:
1 | (lldb) po 0x4065400000000000 |
我们发现竟然没有打印170,这是为什么呢?
原因:16进制和double/float类型的转换
验证:
1 | (lldb) p/x (double)170 |
我们可以得到和上面的地址一抹抹一样样,还有谁?
5)从打印结果我们可以看出一个问题,两个属性占用内存不是按照定义属性的顺序,这是为什么呢?其实这就是 iOS中的内存字节对齐现象
内存对齐的原因
对于大部分程序员来说,内存对齐
对他们来说都应该是 透明的
。
平台原因(移植原因)
:不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐
的内存,处理器需要作两次
内存访问;而对齐
的内存访问
仅需要一次
访问。
内存对齐三大原则
每个特定平台上的编译器都有自己的默认 “对齐系数”(也叫对齐模数)
。程序员可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变这一系数,其中的n就是你要指定的 “对齐系数”。在 ios
中,Xcode默认为 #pragma pack(8),即 8字节对齐
数据成员对齐规则:结构(
struct
)或联合(union
)的数据成员,第一个数据成员放在offset
为0的地方,以后每个数据成员存储的起始位置要从该成员的大小或子成员大小的整数倍开始存储(比如int 为4字节
,那么他的起始位置则为4*n
)结构体作为成员:如果⼀个结构⾥有某些结构体成员,则结构体成员要从其内部最⼤元素⼤⼩的整数倍地址开始存储.(
struct a
⾥存有struct b
,b
⾥有char,int ,double
等元素,那b
应该从8
的整数倍开始存储.)结构体总体大小,必须是其内部最大成员的整数倍,不足要补齐
数据类型所占字节
获取内存的三个方法
sizeof
是运算符,编译的时候就替换为常数,返回的是一个类型所占内存的大小
class_getInstanceSize
传入一个类对象,返回一个对象的实例至少需要多少内存
,它等价于sizeof
,需要导入#import <objc/runtime.h>
malloc_size
返回系统实际分配的内存大小
,需要导入#import <malloc/malloc.h>
对象属性内存对齐
1、首先通过一个类的内存段来分析
2、然后我们下断点,分析一下对象的内存情况
po p
:查看当前的对象信息<ZJPerson: 0x60000072f870>- ZJPerson为当前的类,
0x60000072f870
为当前对象的首地址
- ZJPerson为当前的类,
iOS为
小端
模式,内存要倒着读
,即 0x0000 0001 0950 2730,不足16位要补0,而0x0000 000109502730
是指针isa
,指向内存的首地址0x60000072f870
x p
:意思是查看这个地址的内存情况,即内存段由于小端模式不好读取,所以我们用一个命令自动帮我们整理好
x/6gx
等等x/6xg p
:意思就是按照16进制,以6整段打印当前p对象
我们发现OC为我们做了一些优化,我们发现0x0000001200006261这个内存段存储了
age,char1,char2
,三个属性- 我们
po 0x00000012
打印30
,也就是我们的age
属性值 po 0x61 0x62
打印的分别为97 98
也就是我们a和b对应的ASCII
码
- 我们
3、为什么 age、c1、c2
三个属性放在一个 8字节
内存中呢?
iOS系统不是对每一个属性都开辟8个字节内存空间,如果每一个属性都开辟8个字节,会造成内存浪费
iOS系统通过内存对齐方式,对 属性重排,内存优化
,
结构体内存对齐
- 结构体指针的内存大小8
- 结构体大小根据内部大小来计算
我们都知道 对象
的 本质
是 结构体
,因此 对象
的 内存对齐
来自 结构体
,因此接下来我们分析一下机构体是如何内存对齐的:
1、struct1
1 | // 普通结构体 |
我们通过图来分析一下struct1的内存存储情况:
根据内存对齐规则,分析ZJStruct1的内存计算:
变量a
:占8
个字节,从0
开始,此时0
存储a
变量b
:占1
个字节,从8
开始,此时8
可以整除8
,即8-15
存储b
变量c
:占4
个字节,从9
开始,此时9
不可以整除8
,往后移动到12
,12
可以整除4
,即12-15
存储c
变量d
:占2
个字节,从16
开始,此时16
可以整除2
,即16-17
存储d
因此ZJStruct1的需要的内存大小为 18
字节,而ZJStruct1中最大变量的字节数为 8
,所以 ZJStruct1 实际的内存大小必须是 8
的整数倍,18
向上取整到 24
,主要是因为 24
是 8
的整数倍,所以 sizeof(ZJStruct1)
的结果是 24
2、struct2
1 | // 普通结构体 |
我们通过图来分析一下struct1的内存存储情况:
根据内存对齐规则,分析ZJStruct2的内存计算:
变量a
:占1
个字节,从0
开始,此时0
存储a
变量b
:占4
个字节,从1
开始,此时1
不可以整除4
,往后移动到4
,此时4
可以整除4
,即4-7
存储b
变量c
:占2
个字节,从8
开始,此时8
可以整除2
,即8-9
存储c
变量d
:占8
个字节,从10
开始,此时10
不可以整除2
,往后移动到16
,16
可以整除8
,即16-23
存储d
因此 ZJStruct2
的需要的内存大小为 24
字节,而ZJStruct1中最大变量的字节数为 8
,所以 ZJStruct1
实际的内存大小必须是 8
的整数倍,24
向上取整到 24
,主要是因为 24
是 8
的整数倍,所以 sizeof(ZJStruct2)
的结果是 24
3、struct3
1 | // 嵌套结构体 |
我们通过图来分析一下struct3的内存存储情况:
根据内存对齐规则,分析ZJStruct3的内存计算:
变量a
:占8
个字节,从0
开始,此时0-7
存储a
变量b
:占4
个字节,从8
开始,此时8
可以整除4
,即8-11
存储b
- 结构体成员
s1
:s1
是一个结构体,根据内存对齐原则二
,结构体成员要从其内部最大成员
大小的整数倍开始存储,而ZJStruct3中最大的成员大小为4
,所以s3
要从4
的整数倍开始,当前是从12
开始,所以符合要求,12
是4
的整数倍,符合内存对齐原则,所以12
开始变量a
:占1
个字节,从12
开始,此时12
可以整除4
,即12
存储a
变量b
:占4
个字节,从13
开始,此时13
不可以整除4
,往后移动到16
,此时16
可以整除4
,即16-19
存储b
变量c
:占2
个字节,从20
开始,20
可以整除2
,即20-21
存储c
- 由于结构体内部的字节对齐原则
4
的整数倍,后面要补上22,23
,此时12-23
正好是4
的整数倍,即4*3
变量c
:占2
个字节,从24
开始,24
可以整除2
,即24-25
存储c
因此ZJStruct4的需要的内存大小为 26
字节,而ZJStruct4中最大变量的字节数为 8
,所以 ZJStruct4 实际的内存大小必须是 8
的整数倍,26
向上取整到 32
,主要是因为 32
是 8
的整数倍,所以 sizeof(ZJStruct4)
的结果是 32
4、我们通过上面的分析来实际验证一下结果:
1 | // 打印结构体内存大小 |
结果显示我们分析的结果是正确的
内存优化(属性重排)
如果是结构体中数据成员是根据内存
从小到大的顺序定义
的,根据内存对齐规则来计算结构体内存大小,需要增加有较大的内存padding即内存占位符,才能满足内存对齐规则,比较浪费内存
如果是结构体中数据成员是根据内存
从大到小的顺序定义
的,根据内存对齐规则来计算结构体内存大小,我们只需要补齐少量内存padding即可满足堆存对齐规则,这种方式就是苹果中采用的,利用空间换时间
,将类中的属性进行重排,来达到优化内存的目的
以下面这个例子来进行说明 苹果中属性重排,即内存优化:
1、我们通过自定义一个类ZJPerson,并定义几个属性,来分析一下
1 | @interface ZJPerson : NSObject |
2、在main中创建ZJPerson的实例对象,并对其中的几个属性赋值
1 | #import <Foundation/Foundation.h> |
3、断点调试p,根据ZJPerson的对象地址,查找出属性的值:
- 当我们向通过
0x0000001e00006261
地址找出age
等数据时,发现是乱码
,这里无法找出值的原因是苹果中针对age、c1、c2
属性的内存进行了重排,因为age
类型占4
个字节,c1
和c2
类型char
分别占1
个字节,通过4+1+1
的方式,按照8
字节对齐,不足补齐的方式存储在同一块内存中
- 我们通过
po 0x000000010d671058
找出name
的值ZJ
,通过po 0x000000010d671078
找出nickName
的值小J
4、所以,这里可以总结下苹果中的内存对齐思想:
大部分的内存都是通过固定的内存块进行读取,尽管我们在内存中采用了内存对齐的方式,但并不是所有的内存都可以进行浪费的,苹果会自动 对属性进行重排
,以此来 优化内存
字节对齐到底采用多少字节对齐?
到目前为止,我们在前文既提到了 8
字节对齐,也提及了 16
字节对齐,那我们到底采用哪种字节对齐呢?
我们可以通过 objc4
中的源码来进行分析
1、实例对象
开辟内存的流程如下:
1)class_getInstanceSize
内部实现如下:
1 | /** |
class_getInstanceSize
内部调用alignedInstanceSize
1
2
3
4// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}word_align
内部调用如下:
1 | #ifdef __LP64__ |
由源码可知:(x + WORD_MASK) & ~WORD_MASK
是一个运算,代表 8
字节对齐
2、为什么对象真正开辟内存是8字节对齐?系统为对象开辟的是16字节对齐?
apple系统为了 防止一切的容错
,采用的是 16
字节对齐的内存,主要是因为采用 8
字节对齐时,两个对象的内存会紧挨着,显得 比较紧凑
,而 16
字节 比较宽松
,利于苹果以后的扩展。
8字节内存计算
由断点可知此时的x为40,WORD_MASK为7,我们来计算一下:(x + WORD_MASK) & ~WORD_MASK
1 | 2^5 = 32 2^4 = 16 2^3 = 8 2^2 = 4 2^1 = 2 2^0 = 1 |
由结果40
可知,与class_getInstanceSize
获取实例对象的大小40
如何吻合,还有谁?
总结
class_getInstanceSize
:是采用8
字节对齐,参照的对象的属性内存大小malloc_size
:采用16
字节对齐,参照的整个对象的内存大小,对象实际分配的内存大小必须是16
的整数倍
- Post title:OC底层原理06:内存对齐探索
- Post author:张建
- Create time:2020-09-14 02:05:22
- Post link:https://redefine.ohevan.com/2020/09/14/OC底层原理/OC底层原理06:内存对齐探索/
- Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.