1.内存对齐
有过计算机组成原理和操作系统学习经验的同学可以知道,在现实中并不会出现像题目中‘人工智能’般的内存申请、分配,不可能不同类型的数据一个个挨着紧密无间不留一点空隙,这样对于机器来说是无法快速读取的而且对于开发也不好拓展,因此机器需要一个规则来存放和读取内存数据,内存对齐原则就是这种类型的规则。
2.数据类型所占大小
在iOS中,每个不同类型的数据都规定了相应的内存大小:
常用数据类型 | 32位机器(4字节) | 64位机器(8字节) |
---|---|---|
char | 1 | 1 |
BOOL | 1 | 1 |
short | 2 | 2 |
int | 4 | 4 |
unsigned int | 4 | 4 |
long | 4 | 8 |
unsigned long | 4 | 8 |
long long | 8 | 8 |
unsigned long long | 8 | 8 |
float | 4 | 4 |
double | 8 | 8 |
NSInteger | 4(=int) | 8(=long) |
NSUInteger | 4=(unsigned int) | 8(=unsigned long) |
point | 4 | 8 |
对于指针类型来说64位机上占8字节,但是这个8字节并不是指指针所指内存大小,而是存贮这个指针内存的大小是固定8字节。而其他值类型就如表所示大小。
表中并没有给出Struct
内存大小,因为结构体由于内部成员数目和类型都不缺定,所以不能给出定值,那它是否是所有成员占字节数目之和呢,按照第一节的介绍,也不可能是简单的求和,按照alloc分析、源码调试(3)分析的结论,提出猜想:ios对于Struct
的内存占用规则是以8进制或者16进制对齐来实现。
3.结构体内存申请的探究
猜想需要验证,那就来验证一波,上代码。
struct PPBox{
char a; //1
double width; //8
int length; //4
short height; //2
}PPBox;
struct PPWindow{
double width; //8
int length; //4
short height; //2
char a; //1
}PPWindow;
struct PPBox box;
box.a = 'a';
box.width = 1.0;
box.length = 2.0;
box.height = 3.0;
struct PPWindow window;
//24
NSLog(@"%lu",sizeof(box));
//16
NSLog(@"%lu",sizeof(window));
可以得到打印结果分别是24和16。同样的数据类型只是顺序变了就导致结构体内存大小不一样,但不管是24还是16都不是 double(8字节)+int(4字节)+short(2字节)+char(1字节) = 15字节这种简单求和方式,对于文章第一条对齐的方式确实存在是可以验证的。
其次,可以增删改数据类型后会发现是以8的倍数出现,故这个结构体内存对齐是以8字节对齐的,这个也是可以确定的。
根据网上搜索可以得到结构体内存对齐的规则有如下三点:
1:结构(struct)的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int为4字节,则要从4的整数倍地址开始存储。 min(当前开始的位置m,n) m=9 n=4 9 10 11 12。
2:结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.)
3:收尾工作:结构体的总大小,也就是sizeof的结果,.必须是其内部最大成员的整数倍.不足的要补⻬。
看不懂,太麻烦,那我直接在这里对PPBox
进行验证:
PPBox
:
第一个是char a
占1个字节,offset
变为1,
第二个是double width
占8字节,当前offset
为1,8%1=0,所以double width
存储的起始位置1,占8字节,offset
变为9。
第三个是int length
占4字节,当前offset
为9,9%4 = 1不为0,所以offset
+1继续判断,当offset
为12时可以整除,即起始位置为12,占4字节,offset
变为16。
第四个是short height
占2字节,当前offset
为16,可以整除2,所以可以直接存放,offset
变为22。
第一步结束,第二部不涉及到,第三部收尾22要是最大成员double width
8字节整数倍,因此补充至24。可以的,没毛病~。
对PPWindow
进行验证:
PPWindow
:
第一个是double width
占8字节,直接存放,offset
变为7。
第二个是int length
占4字节,7%4=3不为0,offset
+1,当offset
为8时可以整除,故以这个位置开始存放,offset
变为12。
第三个是short height
占2字节,7%4=3不为0,offset
+1,当offset
为8时可以整除,故以这个位置开始存放,offset
变为12。
第四个是char a
占1字节,12可以整除,故以这个位置开始存放,offset
变为13。
第一步结束,第二部不涉及到,第三部收尾13要是最大成员double width
8字节整数倍,因此补充至16。可以的,也没毛病~。
后续也可以验证下不同类型成员时是否满足。
4.结构体嵌套内存对齐
由于3条原则中有一条是针对嵌套类型的结构体的内存对齐的,因此也需要研究一下这个情形的,鉴于之前有代码首先偷个懒:
struct PPBox{
char a;
double width;
int length;
short height;
}PPBox;
struct PPWindow{
double width;
int length;
short height;
char a;
}PPWindow;
struct PPBox1{
char a;
double width;
int length;
short height;
struct PPWindow window;
}PPBox1;
struct PPWindow1{
double width;
int length;
short height;
char a;
struct PPBox box;
}PPWindow1;
NSLog(@"%lu",sizeof(box));
NSLog(@"%lu",sizeof(window));
运行输出新的两个结构体都是40,上一章3就知道,原版PPBox
是24字节,PPWindow
是16字节,40=24+16,so easy?
重新写个再验证一下:
struct PPMiniBox{
int qrCode;
struct PPBox box;
}PPMiniBox;
PPBox
中最大字节数是double width
,PPMiniBox
中只有一个int qrCode
最大4字节,最终输出的结果是32,走一遍规则试试:
PPMiniBox
:
第一个是int qrCode
占4字节,offset
变为4,
PPBox
中第一个是char a
占1个字节,4可以整除,但原则二中起始地址必须是最大字节数的整数倍,即8的整数倍,offset
变为8,存1字节变为9,
PPBox
中第二个是double width
占8字节,当前offset
为9,9%8不为0,所以offset
+1,知道变为16可以整除,存放,占8字节,offset
变为24。
PPBox
中第三个是int length
占4字节,当前offset
为24,24%4 = 0,所以offset
为24可以存放,占4字节,offset
变为28。
PPBox
中第四个是short height
占2字节,当前offset
为28,可以整除2,所以可以直接存放,offset
变为30。
第一步完成,第二步也遵循了,第三步收尾,最大字节数为8,offset
变为整数倍就变为32。
与原则对应~
5.实际内存的分析
先对结构体赋值:
struct PPBox{
char a; //1
double width; //8
int length; //4
short height; //2
}PPBox;
struct PPWindow{
double width; //8
int length; //4
short height; //2
char a; //1
}PPWindow;
struct PPBox box;
box.a = 'a';
box.width = 1.0;
box.length = 2.0;
box.height = 3.0;
struct PPWindow window;
window.a = 'b';
window.width = 4.0;
window.length = 5.0;
window.height = 6.0;
对PPBox
和PPWindow
的真实内存情况分析:
PPBox
:总计占用24字节。
首先,
box
中的第一个是char a
可以知道'a'
的ASCII值是61,这里0x0000000000000061
这个整个8字节存的就是char a ='a'
这个值,window
可以发现一个0x0062
就是char a ='b'
这个值。
其次,
box
的第4个8字节和window
的第一个8字节是一样的值,通过左边的指针地址也和发现是同一块,所以这两个结构体内存是连着的。box
占3个8字节,window
占2个8字节。
对
box
分析:第一个8字节,'a'
就占了8字节,和之前的规则还是有些不同的,对齐的大多数字节空间主要是用来填补这里了。
第二个8字节,
0x3ff0000000000000
按照原理应该是表示double width = 1
,这个值但是为什么是这样,这里涉及到double
类型的移码,表示1
的移码,同样的0x4010000000000000
表示的是4的移码,移码
是双精度浮点型的二进制保存形式,这个移码
这里不展开,后续拓展。
第三个8字节,可以明显看见
0x00000003
,0x00000002
两个值,但是为什么不是先0x00000002
再0x00000003
,这里就是涉及到内存的大小端存储模式:
大端模式:是指数据的高字节保存在内存的低地址中,而低子节数据保存在内存的高地址中。
小端模式:是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中。
0x00000003
在结构体中排在0x00000002
的后面相当于低字节,所以保存在低地址中,即0x7ffeefbff480-0x7ffeefbff488
这段中,0x7ffeefbff489-0x7ffeefbff48f
这段存0x00000002
。
同样的道理分析window
:
占16字节,
第一个8字节,0x4010000000000000
表示的width
值为4,同样是移码。
第二个8字节,这次我们采用小端读法,4字节int
类型存放了5这个值,2字节'short'类型存放了6这个值,最后一字节char
类型存放ASCII值是62即'b'
这个值,刚刚好。
6.类的内存对齐探究
新建一个Person
类
@interface Person : NSObject
@property (nonatomic,assign) int age;
@property (nonatomic,strong) NSString *nickname;
@property (nonatomic,assign) float height;
@property (nonatomic,strong) NSString *name;
@end
Person *person = [Person alloc];
person.age = 10;
person.nickname = @"pp";
person.height = 180.0;
person.name = @"ppext";
NSLog(@"%lu",sizeof(person));
通过isa的相关分析可以知道isa与对象、类的关系
,对象的第一个8字节必定存放的是isa
指针。
大致可以估计一下占用内存大小isa
8字节,age
4字节,nickname
8字节,offset
偏移至24,height
8字节,name
8字节,offset
偏移至32,占用32字节。
isa
指针,其他自定义属性信息在后24字节中,对三个8字节强转打印:
0x0000000100004070
表示的是name
属性,0x0000000100004090
表示的是nickname
属性,那不用猜强转显示不出来的就是age
和height
整合了的,0x433400000000000a
小端读取,0x0000000a
就是10表示age
,0x43340000
应该就是180
的移码,至此分析完成,也符合规则。
但是这里也有个问题,内存存放属性的顺序和定义的并不一样,不是先
nickname
和height
的顺序不对,是iOS为了内存对齐,优化存储对属性顺序重排了,将两个4字节组合到一段8字节中,适当调整顺序。