1.对象的内存情况
我们使用LGPerson作为例子
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;
@property (nonatomic) char c1;
@property (nonatomic) char c2;
@end
查看对象的内存情况,我们可以使用在lldb中用的x,p和po这些指令来查看
从上图中可以看到0x600003440ea0为对象指针的首地址,每一行开头部分都是从这个内存地址开始排列的。而0x000000010e2f28d8是对象指针地址,打印也看出来了。我们后面打印地址存储的Cooci,KC都出来了,但是我们复制的18,a,b都去哪了
解释下p,po,x/8gx的意思。p是expression - 的别名,p会返回值的类型以及命令结果的引用名。 po是expression -O — 的别名,po只会输出对应的值。x/8gx 对象表示输出8个16进制的8字节地址空间(x表示16进制,8表示8个,g表示8字节为单位,等同于x/8xg 对象)
下面我们这么打印
打印结果我们发现,我们将之前的地址0x0000001200006261拆分为:0x00000012,0x62,0x61分别打印,我们就得到了。其中97,98分别为小写字母a,b的ASCII编码,通过p/c给转回来。为什么会这样呢,这就牵扯到下面要说的对象内存优化(内存对齐)。
2.内存对齐
先说下内存对齐原则
- 数据成员对齐规则:结构体(struct)或者联合体(union)的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从
该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int为4字节,则要从4的整数倍地址开始存储)。 - 结构体作为成员:如果一个结构体里有某些结构体成员,则结构体成员要从其
内部最大元素大小的整数倍地址开始存储 - 收尾工作:结构体的总大小,也就是sizeof的结果,必须是其
内部最大成员的整数倍,不足的要补齐。
上面的规则注意点我用不同样式标注出来了:1.是该成员或者子成员大小的整数倍开始。2.结构体要从其内部最大元素大小的整数倍开始。2.整体大小也是其内部成员的整数倍。
讲解内存对齐原则理解前,我们先了解下每个属性的字节大小
| OC | 32 | 64 |
|---|---|---|
| bool) | 1 | 1 |
| BOOL | 1 | 1 |
| char | 1 | 1 |
| short | 2 | 2 |
| int | 4 | 4 |
| flot | 4 | 4 |
| long | 4 | 4 |
| NSInteger | 4(int) | 8(long) |
| CGFloat | 4(int) | 8(double) |
| 指针 | 4 | 8 |
| double | 8 | 8 |
| long long | 8 | 8 |
下面开始对内存对齐原则进行理解
下面举例子
struct LGStruct1 {
double a; // 8
char b; // 1
int c; // 4
short d; // 2
}struct1;
每个字节大小我写上去了,我们开始计算需要多少内存大小,a:8字节,从0开始存,是【0-7】8字节,b则是从8开始,由于它是1字节,起始位置8是1的整数倍,故可存【8】1字节,c则是从9开始存,它是4个字节,由于9不是4的整数倍,那么这个起始位置不能使用,要继续向下找,到12,12是4的整数倍,可存【12-15】4字节,d则从16开始,它是2个字节,由于16是2的倍数,可存【16-17】2字节。那么整体下来内部需要大小为17,因为其内部成员变量最大为8,所以整体需要的是8的整数倍既:24.
再看一个例子:
struct LGStruct2 {
int a; //4
char b; //1
short c; //2
}struct2;
这次直接算:a-【0-3】,c-【4】,c-【6-7】,其内部需要的大小是7,但是其内部成员变量最大4,所以整体需要的是4的倍数既:8.
再看一个例子:
struct LGStruct3 {
char a; //1
int b; //4
short c; //2
struct LGStruct4 {
int a; // 4
double b; // 8
char c; // 1
short d; // 2
}struct4;
}struct3;
这次例子是结构体嵌套结构体,他的是多少呢?我们开始:a-【0】,b-【4-7】,c-【8-9】,下面进LGStruct4,a-【12-15】,b-【16-23】,c-【24】,d-【26-27】,那是不是可以认为这个struct3需要大小为28了,28跟8的倍数对比,应该是32
上面的结果是错误的,原因很简单,没有考虑内存对齐,LGStruct4开始的起止位置应该是其内部最大值8的倍数(想想苹果为什么要内存对齐,就明白了)。所以LGStruct4应该是:a-【16-19】,b-【24-31】,c-【32】,d-【34-35】此时需要大小为36,整体也要是内部最大值8的倍数,故应该是40.
验证下结果:
符合我们的计算
由上面可知:结构体的内存对齐是按照属性排下来,但对象的内存对齐却不是的,这是因为编译器对对象内存做了优化,至于怎么优化的,我们继续看
3.对象内存的优化
上篇文章OC底层原理之-对象alloc理解(上篇没排版,谅解😂)我们知道对象在alloc的时候,会向系统申请内存如下图
上面红框中的方法是计算需要开辟多少内存,下面红框方法是去内存中申请内存
我们在下面的红框上打断点发现进不去了,这是因为calloc的源码在libmalloc中,这个也是可以在苹果的开源库中下架。objc的源码跟malloc的源码是分开的
我们打开libmalloc源码,在main函数中写如下代码:
点击calloc进入
在点击malloc_zone_calloc方法
因为返回的是ptr,所以红框内是关键的信息,
但是这个方法也是calloc,跟最开始进来一样,此时在点击就没有什么意义了,此时我们可以使用po命令来查看
我们可以看到返回default_zone_calloc在malloc.c的331行,我们找到这一行下断点,发现又来到calloc,继续打印
找到位置后,下断点,发现进入_nano_malloc_check_clear,而下面就是返回p,所以这句是关键
继续打断点,就发现获取内存大小的方法了segregated_size_to_fit,进方法看看
进这个方法里面,打印size就是我们穿进来的40
注意上图红框内的方法,NANO_REGIME_QUANTA_SIZE是16,SHIFT_NANO_QUANTUM是4。
第一个红框是size+16-1结果右移4,第二个红框是将上个方法得到的结果,再向左移4,具体过程如下
此时我们得到的slot_bytes就为48,返回的也就是16的倍数48
4.总结与思考
通过上面我们了解到这些,对象属性在存储的时候是按照属性中最大值得倍数对齐(一般为8),而16字节对齐则是针对整个对象,为什么会这样呢?因为系统开辟的内存如果按照属性大小来分配,可能会导致内存溢出。测试我们知道结构体内部的属性排列不同,所需要的内存大小也不同。我们在日常开发中,对属性的排列顺序是否可以注意一下,然后使其申请的内存最少,积少成多,整个项目的下来,就会优化不少。
思考:在查资料以及自己实现的时候发现一个有意思的问题:
发现一个是8,一个16这是为什么。malloc_size我扒拉了一下libmalloc源码,还是对对象分配至少是16的倍数,那class_getInstanceSize呢,我又扒拉了一下objc源码,找到下图的返回方法
其中WORD_MASK为7,换种写法:return (x + 7) & (~7),这个算法保证返回一定是8的倍数。这就是为什么打印上面的结果
5.写到最后
OC对象的内存对齐,就说到这里,有些结论也是自己通过现象做出的判断,如果哪里有不对的地方,还希望大家指出!谢谢