前言
在上篇文章中《iOS底层原理--alloc流程探究》我们提到过一个align16 16字节对齐算法,这是否意味着系统在底层开辟内存时就是以16字节对齐为准则进行开辟的呢?下面我们就正式开始探究内存对齐原则。
获取内存大小的3种方式
我们需要先知道iOS中获取内存大小的3种方式,然后从这3种方式入手,开始探索。
- sizeof()
- class_getInstanceSize()
- malloc_size() 下面我们看一段代码,分析下这3种方式获取内存大小的异同。
YSHStudent *baseStu = [[YSHStudent alloc]init];
baseStu.name = @"iOSer";
baseStu.sex = @"男";
baseStu.age = 28; //int类型
baseStu.height = 172.5; //double类型
NSLog(@"%@\n - sizeof打印:%lu\n - class_getInstanceSize打印:%lu\n - malloc_size打印:%lu",baseStu,sizeof(baseStu),class_getInstanceSize([YSHStudent class]),malloc_size((__bridge const void *)(baseStu)));
我们看下打印结果,会发现3种方法打印出来的内存大小均不相同。
这是为什么呢?那就需要我们了解这3种获取内存方式的具体含义是什么了?
sizeof()
- 是一个判断数据类型或者表达式长度的运算符,而不是一个函数;
- 其作用就是返回一个对象或者类型所占的内存字节数;
- 编译器对 sizeof() 的处理都是在
编译阶段进行。
class_getInstanceSize()
该方法在alloc探究中分析_class_createInstanceFromZone源码实现中,已经进行过简单的了解。
- 该方法是runtime提供的一个API,所以咱们在调用该方法的时候需要先引入
objc/runtime.h; - 其本质是获取创建的对象至少所需的内存大小,8字节对齐。
malloc_size()
该方法获取的是堆空间实际分配给对象的内存大小,并且是16字节对齐。
了解了以上3种方法的本质,我们还需要先了解各个数据类型在内存中占用的内存字节大小,如下图所示:
下面咱们就可以开始分析为什么刚才打印的结果分别为8、40、48了。
-
sizeof()
咱们刚才传入的baseStu是一个对象类型,而对象类型的本质是一个结构体指针,指针在内存中占据8字节,所以sizeof打印出来的就是8。 -
class_getInstanceSize()
YSHStudent成员变量所需的内存空间:8+8+4+8+8(isa)=36,然后8字节对齐,也就是40。 -
malloc_size()
堆空间实际分配给对象的内存大小,并且按照16字节对齐,我们可以看到实际分配的内存大小和实际所需的内存大小并不相等。后面有时间会再出一篇详细malloc源码分析的文章,具体分析一下malloc流程。
结构体内存对齐
为了方便内存对齐的具体原则,咱们先来定义两个简单的结构体,通过分析结构体占据的内存大小作为切入点进行探索。
struct YSHStruct1 {
char a; //1字节
double b; //8字节
int c; //4字节
short d; //2字节
}struct1;
struct YSHStruct2 {
double d; //8字节
int c; //4字节
short b; //2字节
char a; //1字节
}struct2;
NSLog(@"%lu-%lu",sizeof(struct1),sizeof(struct2));
看下打印结果:
通过打印结果,我们发现两个结构体的中变量的数量和类型都是一样的,只是顺序不同,但是所占用的内存大小却完全不同,而这就是因为内存对齐原理造成的。
内存对齐规则
- 结构(struct)或联合(union)的数据成员,第⼀个数据成员放在offset为0的地⽅,以后每个数据成员存储的起始位置要从该成员⼤⼩或者成员的⼦成员⼤⼩(只要该成员有⼦成员,⽐如说是数组,结构体等)的整数倍开始(⽐如int为4字节,则要从4的整数倍地址开始存储)
- 如果⼀个结构⾥包含结构体成员,则结构体成员要从其内部最⼤元素⼤⼩的整数倍地址开始存储(struct a⾥存有struct b,b⾥有char、int、double等元素,那b应该从8的整数倍开始存储)。
- 结构体的内存的总⼤⼩,必须是其内部成员最大内存的整数倍,不⾜的需要补⻬。 下面我们把上面👆的例子按照内存对齐规则进行分析说明。
结构体struct1内存大小计算
变量a 为char类型,占用1个字节,从0开始排放即【0】
变量b 为double类型,占用8个字节,按照内存对齐原则,需要从8的倍数开始放,显然1不是8的倍数,直到找到8开始排放即【8-15】,中间的【1-7】均是补齐。
变量c 为int类型,占用4个字节,从16开始排放,16恰好是4的倍数,即【16-19】
变量d 为short类型,占用2个字节,从20开始排放,即【20-21】
struct1中最大的变量b所需字节数是8字节,struct1实际内存必须是8的倍数,22向上取整为24,所以sizeof(struct1)的结果是24
stuct1内存分布概念图如下所示:
结构体struct2内存大小计算
变量d 为double类型,占用8个字节,从0开始排放即【0-7】
变量c 为int类型,占用4个字节,从8开始排放,8恰好是4的倍数,即【8-11】
变量b 为short类型,占用2个字节,从12开始排放,12恰好也是2的倍数,即【12-13】
变量a 为char类型,占用1个字节,从14开始排放即【14】
struct1中最大的变量b所需字节数是8字节,struct1实际内存必须是8的倍数,15向上取整为16,所以sizeof(struct2)的结果是16
stuct2内存分布概念图如下所示:
分析完这个,下面我们难度升级,探索下结构体嵌套结构体,内存空间又是如何计算内存大小的?
结构体嵌套结构体
话不多说,先来定义一个结构体嵌套结构体的结构体,如下所示:
struct YSHStruct2 {
double d;
int c;
short b;
char a;
}struct2;
struct YSHStruct3 {
double d;
int c;
short b;
char a;
struct YSHStruct2 stru;
}struct3;
NSLog(@"struct3内存大小:%lu\n struct3中结构体stru成员内存大小:%lu",sizeof(struct3),sizeof(struct3.stru));
下面我们看下输出结果:
我们根据内存对齐规则,来分析下结构体嵌套结构体的内存大小的计算过程
变量d 为double类型,占用8个字节,从0开始排放即【0-7】
变量c 为int类型,占用4个字节,从8开始排放,8恰好是4的倍数,即【8-11】
变量b 为short类型,占用2个字节,从12开始排放,12恰好也是2的倍数,即【12-13】
变量a 为char类型,占用1个字节,从14开始排放即【14】
结构体stru 为结构体类型,根据内存对齐规则第2条,stru中变量占据内存最大的变量b所需的字节是8,所以必须从8的整数倍开始存放stru,显然15不是8的倍数,接着往后找,发现16是8的整数倍,即【16-31】
这里我最开始有一个疑惑,结构体内存需要是内部成员中最大变量占的字节数的整数倍,struct3中最大的成员不是stru吗? 它所占的字节数是16,而32又正好是16的倍数,完全符合内存对齐规则啊。所以我就在struct2中再添加一个double类型的变量来验证这个观点。
struct YSHStruct2 {
double d;
int c;
short b;
char a;
double e;
}struct2;
struct YSHStruct3 {
double d;
int c;
short b;
char a;
struct YSHStruct2 stru;
}struct3;
NSLog(@"struct3内存大小:%lu\n struct3中结构体stru成员内存大小:%lu",sizeof(struct3),sizeof(struct3.stru));
看下打印结果:
从这个打印结果来看,我们得出一个结论,当结构体嵌套了结构体时,作为数据成员的结构体的最大成员内存大小作为外部结构体的最大成员的内存大小,而不是以作为数据成员的结构体大小作为外部结构体的最大成员内存大小。
因此结构体嵌套的第一个示例中struct3中最大变量为stru, 其最大成员内存字节数为8,根据内存对齐原则,所以struct3实际的内存大小必须是8的整数倍,32正好是8的整数倍,所以sizeof(struct3)的结果是32。
struct3的内存分布概念图如下所示:
内存优化
不知道之前有没有发现,当一个类有多个属性的时候,并不是完完全全按照字节对齐进行内存排布的,下面我们写个demo验证一下。
YSHStudent *baseStu = [[YSHStudent alloc]init];
baseStu.name = @"iOSer";
baseStu.sex = @"男";
baseStu.age = 28;
baseStu.height = 172.5;
baseStu.a = 'a';
baseStu.b = 'b';
baseStu.hobby = @"coding";
NSLog(@"-------%@",baseStu);
通过LLDB调试,根据baseStu的地址,打印属性的值。
我们发现系统将a/b/age字段进行了重排存储在同一个内存块中。这应该就是苹果系统内部进行的处理优化,既避免了内存的浪费,又能提高读取效率和安全访问。
16字节对齐算法
目前我们知道的字节对齐算法有两种。
- align16算法
我们上篇文章讲到过align16算法,这里就不再多加赘述。
static inline size_t align16(size_t x) {
//16字节对齐算法 &为与操作 ~为取反操作
return (x + size_t(15)) & ~size_t(15);
}
- segregated_size_to_fit算法
#define SHIFT_NANO_QUANTUM 4
//将1 左移4位 0000 0001 -> 0001 0000 也就是16
#define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM) // 16
static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
size_t k, slot_bytes;
if (0 == size) {
size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
}
//核心算法
k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
slot_bytes = k << SHIFT_NANO_QUANTUM; // multiply by power of two quanta size
*pKey = k - 1; // Zero-based!
return slot_bytes;
}
这两行代码是算法的核心,我们来翻译一下。
k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM;
k= 【(size + 16 - 1) 右移4位】
slot_bytes = k << SHIFT_NANO_QUANTUM;
slot_bytes = k左移4位
举个例子验证一下,假设 size=20
20+16-1=35 -> 0010 0011
右移4位 -----> 0000 0010
再左移4位 ---> 0010 0000 = 32
总结
- 对象内部来说,也就是属性和属性之间,其真正的对齐方式是
8字节对齐,目前所知的最大类型占用的内存也就8字节。 - 对象外部来说,也就是对象和对象之间,采用的是
16字节对齐方式,提高内存读取容错率。 - 系统内部会对部分类型的数据进行重排,进行内存优化。