在开始之前,我们先了解下接下来会用到的 lldb 调试指令:
po 打印信息
p 打印详细的信息
bt 打出堆
register read 读取寄存器
x 读取内存段
x/4gx 读取4段内存段
接下来,我们还是先举个栗子。
Person 栗子
Person 无属性
此时我们只是创建了 Person 类,其中什么信息都没有。当我们读取内存段,打印出的前8位是 Person,此时使我们的 isa 指针。
接下来我们在 Person 类中生命一些属性,代码如下:
Person 增加 NSString 属性
// Person 类中的属性
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@end
通过以上的 lldb 调试,我们可以发现,每一个 NSString 类型的属性是占8位的大小。
当我们在 Person 类中增加其他类型的属性呢?
那先加个 int 类型分析下吧...
Person 增加 int 属性
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) int age;
@end
咦,发现和我们上面的只有 NSString 类型的属性的时候不一样了,为什么第二个8字节的内存段会打印出 0x0000000000000000 呢?
不要急,让我们赋个值看看...
是不是发生了神奇的事情,为什么最后 age 反而跑到前面了呢?不要急不要急,我们再看下更神奇的东西。
再次在属性中增加个 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字节的内存呢?
我们知道 int 类型是占 4 个字节的,所以两个 int 就是 8 个字节哟。
那么我们在搞点事情,在 Person 类中增加一个 char 属性
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 a;
@property (nonatomic) char b;
@end
通过 lldb 调试,我们可以发现,一个 int 类型的和两个 char 类型的属性共用了 8 字节的内存空间;
Why?
这就是进行了优化重排了;那具体是为什么呢?是因为我们对象在底层其实就是一个结构体,而结构体会有一个内存对齐的机制。
接下来才是我们的重点了!!!
内存对齐
我们先分析下下面两个结构体的大小是什么;
struct Struct1 {
double a; // 8
char b; // 1
int c; // 4
short d; // 2
}struct1;
struct Struct2 {
double a; // 8
int b; // 4
char c; // 1
short d; // 2
}struct2;
如果按照字节来算的话,是不是每个都是15个字节呢?
作为一个开发人员,肯定不能就这么简单的计算呀。那下面我们看一下内存对齐的原则:
1、数据成员对⻬规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int为4字节,则要从4的整数倍地址开始存储)。
2、结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储.(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.)
3、收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的要补⻬
看到上面一大段文字,是不是很难阅读下去,下面我们做下总结,还是举栗子来分析。
还是以上面的两个例子来分析:
首先我们分析第一个结构体:
struct1,如图中所示,分析结果如下:
- a 是 double 类型,占 8 字节,从 0 号位开始,0 是 8 的倍数,需要占据 【0-7】 号位
- b 是 char 类型,占 1 字节,从 8 号位开始,8 是 1 的倍数,需要占据 【8】 号位
- c 是 int 类型,占 4 字节,从 9 号位开始,9、10、11 不是 4 的倍数,12 是 4 的倍数,需要占据 【12-15】 号位
- d 是 short 类型,占 2 字节,从 16 号位开始,16 是 2 的倍数,需要占据 【16-17】 号位
struct2,如图中所示,分析结果如下:
- a 是 double 类型,占 8 字节,从 0 号位开始,0 是 8 的倍数,需要占据 【0-7】 号位
- b 是 类型,占 14字节,从 8 号位开始,8 是 4的倍数,需要占据 【8-11】 号位
- c 是 char 类型,占 4 字节,从 12 号位开始,12 是 1 的倍数,需要占据 【12】 号位
- d 是 short 类型,占 2 字节,从 13 号位开始,13 不是 2 的倍数,14 是 2 的倍数,需要占据 【14-15】 号位
上述就是整个结构体内存对齐的流程;
那么我们再来简单的将内存对齐的原则进行下简单的总结:
1、存放数据的起始位置是数据成员字节的整数倍
2、找到最大字节数的成员
3、结构体总大小是最大成员字节数的整数倍
既然知道了整个内存对齐的规则了,那我们来个复杂点的数据结构:
struct Struct1 {
double a;
char b;
int c;
short d;
}struct1;
struct Struct2 {
double e;
int f;
char g;
short h;
struct Struct1 i;
}struct2;
struct2,如图中所示,分析结果如下:
- e 是 double 类型,占 8 字节,从 0 号位开始,0 是 8 的倍数,需要占据 【0-7】 号位
- f 是 类型,占 14字节,从 8 号位开始,8 是 4的倍数,需要占据 【8-11】 号位
- g 是 char 类型,占 4 字节,从 12 号位开始,12 是 1 的倍数,需要占据 【12】 号位
- h 是 short 类型,占 2 字节,从 13 号位开始,13 不是 2 的倍数,14 是 2 的倍数,需要占据 【14-15】 号位
- i 是 结构体,所以需要对里面的属性进行分析,而且是以结构体中最大成员的字节数为倍数
- a 是 double 类型,占 8 字节,从 16 号位开始,16 是 8 的倍数,需要占据 【16-23】 号位
- b 是 char 类型,占 1 字节,从 24 号位开始,24 是 1 的倍数,需要占据 【24】 号位
- c 是 int 类型,占 4 字节,从 25 号位开始,25、26、27 不是 4 的倍数,28 是 4 的倍数,需要占据 【28-31】 号位
- d 是 short 类型,占 2 字节,从 32 号位开始,32 是 2 的倍数,需要占据 【32-33】 号位
tips: 系统在开辟内存的时候,会是 16 字节对齐,但是对象需要的真正内存,确实 8 字节对齐。