iOS的底层探究--------内存对齐

666 阅读9分钟

作为一名技术探究者,别的废话也就不多说了,直接进入主题。。。。开干😁

由于直接给出一个结论太过于干涩,所以这偏文章用了比较多的篇幅来阐述,需要一点小耐心哦

101623228788_.pic.jpg

首先,谈谈对象的内存影响因素

直接上代码,我们首先创建一个LGPerson类,再声明7个成员变量 27AB447C-CF8E-4D80-972D-D41E43B9618F.png

然后在main.m里面调用 354AE6FE-BBE0-4834-ACB9-D367C2F6AA9F.png

运行工程,来到断点处 4F433F42-A33F-45B1-B33F-8D1DB8EC6580.png

注意哦:执行class_getInstanceSize需要导入头文件#import <objc/runtime.h>

然后,我们在LGPerson类中,注释几个成员变量 E9C99CFA-CF0D-413A-9E05-3FE8CF2E3D3B.png

再次运行,此时得到的内存size,变成32 8C2FCA28-116E-40A6-ACFF-B02C8E0F867C.png

当我们再注释一些成员变量 EA57890D-42A1-4AAB-AEBE-11D8FE031630.png

此时的内存size16B5E307C3-141D-4A5A-8DCF-595E643D435F.png

那么,就能得出结论,类的成员变量对内存是有影响的

如果是给LGPerson类添加属性的话 5E8D3C19-117C-4DE0-BAC5-DE0622C4F056.png

再次打印内存size,发现内存size增大 1D89156A-7A0A-491E-939C-32E65F7222EC.png

如果在LGPerson类里面赠一个+(void)sayNB的方法 545EE89D-96A0-4B6E-A50E-2D3D87F8E347.png

运行之后,查看内存size大小。发现没变 4D105EB7-2E1F-45CB-9B8D-39868DB13256.png

那么,我们就可以得出,属性和成员变量,会影响内存size大小,而方法则不会,说明方法不存放在当前对象的内存里面

05270E4D-740D-4A9A-8665-AE60A0685DE5.png

然后,我们打开所有LGPerson的所有成员变量,在main.m里面赋值 61257EDF-0E20-4A11-9DF1-AE19EA33E687.png

使用x/8gx 查看person 的内存和相应的值 B9E53ABB-CB8A-4F57-9944-F2ED0C7E7892.png

从上图我们可以看出,person.age、person.c1、person.c2他们三个成员变量的是存储在一个8字节的空间里面,并没有单独进行存放。就像person.name、person.nickName。。。这几个成员变量,就占了一个8字节的存储空间。这就是苹果对内存进行有优化。虽然,系统的单位内存是一个固定值,但是如果变量的内存过小,或者太大,苹果系统在内存分配时,会做合理的优化处理。

下图是不同类型的内存大小(盗图哈:Cooci 🤩😄) E39D0EEB-8655-490B-8675-5A0169CCAAD2.png

到了这里,就引出一个知识点:结构体内存对齐

结构体内存对齐

就比如下面两个结构体,里面的变量类型换了顺序,就导致结构体所需的内存大小不一样

struct LGStruct1 {
    double a;       // 8    [0 1 2 3 4 5 6 7]占用8字节内存
    char b;         // 1    [8]   1存的是8号位,8号位是1的倍数,所以可以存
    int c;          // 4    (9 10 11 [12 13 14 15]   4从9号位开始存,但是9号位不是4的倍数,那么就的往下找,10号位也不是4的倍数,接着往下找,11号位也不是,接着往下找,12号位是4的倍数,那么就从12号位开始存
    short d;        // 2    [16 17]         得到:24   但是要满足结构体内存对齐原则,是其内部最大成员的整数倍,不足要补齐,那么结构体中,最大的是8,现在结构体内存已经存到了17号位,那么补齐后,就是24,struct1所需的内存大小是24
}struct1;
//struct1结构体所需内存大小为24

//同理:struct2的内存对齐计算
ruct LGStruct2 {
    double a;       // 8    [0 1 2 3 4 5 6 7]
    int b;          // 4    [8 9 10 11]
    char c;         // 1    [12]
    short d;        // 2    (13 [14 15]      达到:16
}struct2;
//struct2结构体所需内存大小为16

//同理:struct3的内存对齐计算
struct LGStruct3 {
    double a;    //8 [0 1 2 3 4 5 6 7]
    int b;       //4 [8 9 10 11]
    char c;      //1 [12]
    short d;     //2 (13 [14 15]
    int e;       //4 [16 17 18 19]
    struct LGStruct1 str; //24 (20 21 22 23 [24 ... 48]    得到:48
}struct3;

内存对齐原则

1、数据成员对⻬规则:结构(struct)(或联合(union))的数据成员,第⼀个数据成员放在offset为0的地⽅,以后每个数据成员存储的起始位置要从该成员⼤⼩或者成员的⼦成员⼤⼩(只要该成员有⼦成员,⽐如说是数组,结构体等)的整数倍开始(⽐如int为4字节,则要从4的整数倍地址开始存储。min(当前开始的位置m、n)  m=9    n=4        9  10  11  12

2、结构体作为成员:如果⼀个结构⾥有某些结构体成员,则结构体成员要从其内部最⼤元素⼤⼩的整数倍地址开始存储.(structa⾥存有structb,b ⾥有char,int,double等元素,那b应该从8的整数倍开始存储.)

3、收尾⼯作:结构体的总⼤⼩,也就是sizeof的结果,必须是其内部最⼤成员的整数倍,不⾜的要补⻬。

直接在工程中验证下: 35E9E785-CE17-4F53-86A9-014025C79DFD.png

果然,struct1所需的内存是24struct2所需的内存是16

下面,画了个草图(真的是草图啊😏),说明结构体内存对齐的重要性。 D2C579DD-0D0F-403E-8540-5A9A1A1349D8.png

这也就是刚刚在上面计算struct1struct2结构体所需内存时,当存储的变量所需内存大小和所存号位不是倍数关系的话,就需要空出来,往下挪移的原因。就是用空间换取时间,优化存储,提高效率。

也就是说,为什么要结构体对齐的原因了。

到这里,就已经把明面上的,在工程里面就能直接通过调试和对比数据,来探究结构体对齐的原因,那么接下来,将从底层,来探究内存对齐的原因

内存对齐

继续开干😄

上代码,创建一个LGPerson类,声明了4个成员变量 210CFB5C-3CE9-4B2E-AA97-5B88B20089DB.png

main.m中初始化LGPerson类,并且给变量赋值 41A7BF58-E136-4C80-9C43-6D6D30B94BD6.png

根据前面,我们计算的方式方法,我们很容易算出personsizeof28,那么person的所需内存,通过内存对齐原则,8字节对齐,就得到是32,但是事实上是这样子吗? 那就大错特错了。

由于person是一个对象,也就是结构体指针,地址指针多占内存就是8字节,所以personsizeof8。其实,刚刚我们算出的28只是单纯的LGPerson的内存,但是被调用之后,还需要加上isa指针的内存大小8,所以,用内存对齐后的内存大小的值32 + 8(isa指针)= 40。那么就得到class_getInstanceSize([LGPerson class])的值为40

那么就剩下malloc_size((__bridge const void *)(person))的值的计算了。这个需要分析malloc_size的源码,所以,我们要到objc的源码中,去查看具体的计算流程。 ACE63CE1-1D6A-48FB-A429-91365CC7E250.png

我们在objc的源码中,还是不能得到我们想要的malloc底层。那我们可以通过另外一个底层源码去查看跟踪,那就是libmalloc源码。(需要libmalloc源码的童鞋,请留言咯😄)

打开libmalloc源码,在main.m里面调用calloc E858C22E-A9E3-4759-A10C-5667B07EA2AC.png

再进入到calloc方法里面查看实现 532614EE-DF10-4E7B-BBF7-491CDF2C4B4B.png

再进入_malloc_zone_calloc方法里面 1DC708EE-F30E-4CD7-9748-3A0755EBA951.png

但是沿着这里的calloc进入,查看,就发现,只是一个声明了 CE0962AE-87A2-4EA0-BF8B-B3E59D11E0D5.png

不要慌,先发个朋友圈,说明下此刻的心情😄

抽根烟,找找灵感,~~ ~~ ~~ ~~ ~ 咦, 有了,可以通过别的方式查看接下来的方法流程

BA485F97-F7AD-494A-9A81-74EDCD935FF2.png

因为直接索引calloc,看到的大多都是关于calloc的声明和赋值,那么有了赋值,就会有存储值,有了存储值,就能打印输出。 那么就能得到default_zone_calloc方法,这时,也就能全文索引这个方法了,执行断点,到达此处。 (还可以通过在代码:ptr = zone->calloc(zone, num_items, size); 处,用汇编调试,找到下一步执行的default_zone_calloc方法)

ED8592DC-E637-4FA8-9E39-496772B61164.png

同样按照刚刚的步骤,再执行一次,发现将会执行 nano_calloc 方法,全文索引 DFE030C7-C9EE-4B35-96AC-C6D8AC1C6ABF.png

根据这个方法的返回值,可以知道需要的是一个total_bytes,根据代码,total_bytes值正常的话,是返回了p,那么就可以查看计算p的方法_nano_malloc_check_clear,进入到_nano_malloc_check_clear方法里面。 DAC0C501-728A-45A8-AFA3-A625D95A20BC.png

那么在进入segregated_size_to_fit方法 0CFFC97F-E180-41A1-87A5-B9E4D8539B3F.png

其中 80804CBD-1B92-4D59-B20C-3A6BBD33A0CC.png

size是我们在代码里面void *p = calloc(1, 40),传入进来了,所以,就是(40 + 16 - 1) >> 4 << 4,也就是16字节对齐。 (16字节对齐:其实际上就是取n * 16 (n为正整数),就比如(m + 15)= sum,m为任意正整数,sum16的整数倍,sum / 16,得到的还是一个整数,余数舍去。这个,就是16字节对齐。)

最终得到的结果是48. 8CDD427D-2A52-496C-B845-B781856C9D9C.png

所以,就能得到结论

在堆区,对象的内存是16字节对齐的;

成员变量内存,是8字节对齐的,也就是结构体内部内存;

从整个系统内存来讲,对象和对象之间,是16字节对齐。

对象和对象之间,是16字节对齐,为什么这么说了?

假如内存中出现野指针、或者内存访问错误。

就比如存储大小为64字节的内容,如果是8字节为单位的话,就有8 16 24 32 40 48 56 64这样的分配效果,8字节单位就回出现7个衔接点;

如果是16字节单位的话,就有16 32 46 64,单位内存块和单位内存块之间,只有3个衔接点,那么读取的时候,相比8字节单位的存储方式出错的概率更小。

但是开发中,任何对象,都是来自于NSObject,其最基本的都是有8字节的,但是在创建时,都会加入其它的内容,所以,都是超过8字节的。如果以8字节为单位对齐的话,计算时出错的概率更大。

如果是用32字节对齐的话,就有32 64,这个衔接点,就更少了,出错的概率也变得更加小,但是,如果存储一个33字节大小的内容,就需要开辟64字节大小的空间,那么就造成了31字节的浪费。

所以,16字节对齐,最为合理。

到了此处,欧耶,大功告成,内存对齐的底层探究,就完成了,有木有点收获啊,(不许没有啊<( ̄▽ ̄)/)

感谢各位的光临~ ~ ~ ~ ~ ~ 71623057628_.pic.jpg