上篇文章中iOS-底层原理:alloc & init & new 源码分析通过对alloc源码的分析,可以得知alloc的主要目的就是开辟内存,并且会通过size = cls->instanceSize(extraBytes)
这一步来计算需要开辟的内存大小。那么它在计算的过程中是怎么处理的呢,或者说要遵循什么样的原则?下面我们来接着探索。
一切的一切我们还是从最初的代码开始
alloc一个无属性Person
此时我们只是创建了Person
类,其中什么信息都没有。当我们读取内存段,打印出的前8
位是 Person
,此时是我们的isa
指针。
Person 增加 NSString 属性
// Person 类中的属性
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@end
- 首先是
无赋值
情况
- 赋值
一个属性
情况
- 赋值
两个属性
情况
通过打印我们得知一个NSString
类型的属性是占8
位字节大小,
光是这点我们看不出什么,接着继续。。
Person 增加 int 属性
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) int age;
@end
- 先看下
无赋值
情况
这时候我们发现和之前只有NSString的时候不一样了,第二个段内存段会打印出 0x0000000000000000 ,接着继续。。
- 把
age
给赋值
这时候我们发现本来第二块的位置应该是name
,现在被age
给占了?怎么不知道先来后到?为了想知道为什么接着往下走。。
再次在Preson
属性中增加个int
类型的属性,并赋值
。
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) int height;
@end
从打印上看此时两个int
类型age
和height
共用一个8字节的内存。
接着往下看。。
Person 增加 char 属性
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) int height;
@property (nonatomic) char s;
@property (nonatomic) char b;
@end
看下打印结果
这时候我们发现
一个 int 类型
的和两个 char
类型的属性共用了 8
字节的内存空间。
看到这里我们脑子里有个大大的?
,为什么会是这种情况,那么今天的重点来了。。
内存对齐
计算机内存都是以字节
为单位划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但是实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4
或8
的倍数,这就是所谓的内存对齐
。
内存对齐的原因
我们都知道内存是以字节
为单位,但是大部分处理器并不是按字节块
来存取内存的.它一般会以2字节,4字节,8字节,16字节
甚至32字节
为单位来存取内存,我们将上述这些存取单位称为内存存取粒度
。
-
CPU
的数据总线宽度决定了CPU
对数据的吞吐量 -
64
位CPU
一次处理64 bit
也就是8
个字节的数据,32
同样,每次处理4
个字节的数据
内存对齐规则
每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#pragma pack(n)
,n=1,2,4,8,16
来改变这一系数,其中的n
就是你要指定的“对齐系数”。在ios
中,Xcode
默认为#pragma pack(8)
,即8
字节对齐。
内存对齐规则可理解为以下三点:
- 数据成员对⻬规则:数据成员的对齐规则可以理解为
min(m, n)
的公式, 其中m
表示当前成员的开始位置,n
表示当前成员所需要的位数。如果满足条件m
整除n
(即m % n == 0
),n
从m
位置开始存储, 反之继续检查m+1
能否整除n
, 直到可以整除, 从而就确定了当前成员的开始位置。 - 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从
其内部最大元素大小的整数倍地址开始存储.(struct a
里存有struct b
,b
里有char,int ,double
等元素,那b
应该从8
的整数倍开始存储.) - 收尾工作:结构体的总大小,也就是
sizeof
的结果,.必须是其内部最大成员变量的整数倍.不足的要补齐.
下表是各种数据类型在ios中的占用内存大小,根据对应类型来计算结构体中内存大小,
上面一大堆都是理论,理解着比较困难,难么下面我们就通过实践来分析。
结构体内存对齐
定义两个结构体
//1、定义两个结构体
struct Struct1{
char a; //1字节
double b; //8字节
int c; //4字节
short d; //2字节
}Struct1;
struct Struct2{
double b; //8字节
int c; //4字节
short d; //2字节
char a; //1字节
}Struct2;
//计算 结构体占用的内存大小
NSLog(@"%lu-%lu",sizeof(Struct1),sizeof(Struct2));
以下是输出结果:
24 - 16
两个结构体乍一看,没什么区别,其中定义的变量 和 变量类型都是一致的,唯一的区别
只是在于定义变量的顺序不一致
,那为什么他们做占用的内存大小不相等
呢?其实这就是iOS中的内存字节对齐
现象
结构体
Struct1
内存大小计算
-
根据内存对齐规则计算
Struct1
的内存大小,详解过程如下:-
变量a
:占1
个字节,从0
开始,此时min(0,1)
,即0
存储a
-
变量b
:占8
个字节,从1
开始,此时min(1,8),1
不能整除8
,继续往后移动,知道min(8,8)
,从8
开始,即8-15
存储b
-
变量c
:占4
个字节,从16
开始,此时min(16,4)
,16
可以整除4
,即16-19
存储c
-
变量d
:占2
个字节,从20
开始,此时min(20, 2)
,20
可以整除2
,即20-21
存储d
-
因此Struct1
的需要的内存大小为 18
字节,而Struct1
中最大变量的字节数为8
,所以 Struct1
实际的内存大小必须是8
的整数倍,18
向上取整到24
,主要是因为24
是8
的整数倍,所以 sizeof(Struct1)
的结果是 24
结构体Struct2
内存大小计算
-
根据内存对齐规则计算
Struct2
的内存大小,详解过程如下:-
变量b
:占8
个字节,从0
开始,此时min(0,8)
,即0-7
存储b
- 变量c:占
4
个字节,从8
开始,此时min(8,4)
,8
可以整除4
,即8-11
存储 c -
变量d
:占2
个字节,从12
开始,此时min(12, 2)
,20
可以整除2
,即12-13
存 储d
-
变量a
:占1
个字节,从14
开始,此时min(14,1)
,即14
存储a
-
因此Struct2
的需要的内存大小为15
字节,而Struct1
中最大变量的字节数为8
,所以 Struct2
实际的内存大小必须是 8
的整数倍,15
向上取整到16
,主要是因为16是8的整数倍,所以 sizeof(Struct2)
的结果是 16
。
结构体嵌套结构体
上面的两个结构体只是简单的定义数据成员,下面来一个比较复杂的,结构体中嵌套结构体的内存大小计算情况
//1、结构体嵌套结构体
struct Struct3{
double b; //8字节
int c; //4字节
short d; //2字节
char a; //1字节
struct Struct2 str;
}Struct3;
//2、打印 Struct3 的内存大小
NSLog(@"Struct3内存大小:%lu", sizeof(Struct3));
NSLog(@"Struct3中结构体成员内存大小:%lu", sizeof(Struct3.str));
打印 的结果如下:
Struct3内存大小:32
Struct3中结构体成员内存大小:16
-
分析
Struct3
的内存计算
根据内存对齐规则,来一步一步分析Struct3
内存大小的计算过程-
变量b
:占8
个字节,从0
开始,此时min(0,8)
,即0-7
存储b
-
变量c
:占4
个字节,从8
开始,此时min(8,4)
,8
可以整除4
,即8-11
存储c
-
变量d
:占2
个字节,从12
开始,此时min(12, 2)
,20
可以整除2
,即12-13
存储d
-
变量a
:占1
个字节,从14
开始,此时min(14,1)
,即14
存储a
-
结构体成员str
:str是一个结构体,根据内存对齐原则
,结构体成员要从其内部最大成员大小的整数倍开始存储
,而Struct2
中最大的成员大小为8
,所以str
要从8
的整数倍开始,当前是从15
开始,所以不符合要求,需要往后移动到16
,16
是8
的整数倍,符合内存对齐原则,所以16-31 存储 str
。
-
因此Struct3
的需要的内存大小为32
字节,而Struct2
中最大变量为str,
其内存字节数为16
,所以 Struct2
实际的内存大小必须是 16
的整数倍,32
正好是16的整数倍,所以 sizeof(Struct3) 的结果是 32
内存优化(属性重排)
Struct1
通过内存字节对齐原则,增加了9
个字节,而Struct2
通过内存字节对齐原则,通过4+2+1
的组合,只需要补齐一个字节即可满足字节对齐规则,这里得出一个结论结构体内存大小与结构体成员内存大小的顺序有关
如果是
结构体中数据成员
是根据内存从小到大
的顺序定义的,根据内存对齐规则来计算结构体内存大小,需要增加有较大的内存padding
即内存占位符,才能满足内存对齐规则,比较浪费内存
。如果是
结构体中数据成员
是根据内存从大到小
的顺序定义的,根据内存对齐规则来计算结构体内存大小,我们只需要补齐少量内存padding
即可满足内存对齐规则,这种方式就是苹果中采用的
,利用空间换时间,将类中的属性进行重排,来达到优化内存的目的
。
到此,我们在回过头来看我们一开始演示的Person类,也能验证苹果中的属性重排
,即内存优化。
- 苹果中针对
age、s、d
属性的内存进行了重排,因为age
类型占4
个字节,s和d
类型char
分别占1
个字节,通过4+1+1
的方式,按照8
字节对齐,不足补齐的方式存储在同一块内存中。
总结
苹果中的内存对齐思想:
- 大部分的内存都是通过固定的内存块进行读取,
- 尽管我们在内存中采用了内存对齐的方式,但并不是所有的内存都可以进行浪费的,苹果会自动对属性进行重排,以此来优化内存
多少字节对齐:
我们上篇文章中提及了16字节对齐,我们在这里提到了8字节对齐那我们到底采用哪种字节对齐呢?
- 对于一个对象来说,其真正的对齐方式 是
8
字节对齐,8
字节对齐已经足够满足对象的需求了apple
系统为了防止一切的容错,采用的是16
字节对齐的内存,主要是因为采用8
字节对齐时,两个对象的内存会紧挨着,显得比较紧凑,而16
字节比较宽松,利于苹果以后的扩展。