iOS 底层原理之 class_getInstanceSize & malloc_size & sizeof解析

707 阅读6分钟

class_getInstanceSize()

// 传入的参数是一个类
class_getInstanceSize(<#Class  _Nullable __unsafe_unretained cls#>)

class_getInstanceSize<objc/runtime>提供的api,所以在使用的使用需要引入该头文件。

image.png

如上图所示,定位到class_getInstanceSize属于libobjc.A.dylib库下objc_class.mm类下794行,打开该源码库,探索底层实现。

image.png image.png

image.png

image.png

对以上4张图的4个方法做下说明

  1. class_getInstanceSize() 传入一个类,并通过cls->alignedInstanceSize()函数计算该类的占用内存大小
  2. cls->alignedInstanceSize() 中间层,获取未对齐的类的内存大小,再进行字节对齐(实现分布在第3, 4步)
  3. unalignedInstanceSize() 获取未对齐的类的内存大小,从注释中可以看出决定类的内存大小的是类的成员变量(May be unaligned depending on class's ivars)。
  4. word_align(x),对x进行8字节对齐。

image.png 如上图所示,这里创建了一个DXJPerson类,class_getInstanceSize(DXJPerson.class) 结果是多少呢? 答案: 40

计算未对齐的类的内存大小 unalignedInstanceSize:
NSString + NSString + long + int + isa(继承自NSObect的隐藏属性isa) = 8 + 8 + 8 + 4 + 8 = 36

字节对齐 word_align(36):
36 + 7 & ~7 = 42 & ~7

  42  0010 1010
& ~7  1111 1000
= 40  0010 1000

malloc_size()

探索malloc_size()就要搞明白calloc()的内部如何计算大小,在探索alloc底层实现中,知道calloc() 属于libsystem_malloc.dylib库,打开该库源码探索实现。

image.png

calloc -> _malloc_zone_calloc() -> zone->calloczone->calloc发现往下看不到实现了,但是我们可以使用po 函数名 注意不要家括号 或者进入汇编模式,如下图: image.png

default_zone_calloc -> zone->calloc 再次看不到源码了,继续使用po 函数名 image.png

nona_calloc 既然是alloc 自然而然就把重点锁定在_nano_malloc_check_clear身上 image.png

探索的路有些长且曲折,但是目的很明确就是想要知道calloc是如何计算内存大小的,segregated_size_to_fit 函数就是在调整你传入的大小 image.png

segregated_size_to_fit 源码实现 image.png 由于segregated_size_to_fit 源码实现,里面宏定义太多了,不便于观看,这里重写了以上方法,把数字替换了宏。

image.png 这里举个例子来理解下这个算法,假使我们传入的大小是36

void *p = calloc(1, 36);

NSLog(@"%lu",malloc_size(p));
36 用二进制表达为: 0010 0100,以下计算我们都使用二进制进行计算
第1步: size_t nano_regime_quanta_size = (1 << 4);
   0000 0001 << 4 = 0001 0000   即: nano_regime_quanta_size = 16

第2步: k = (size + nano_regime_quanta_size - 1) >> 4;
   k = (36 + 16 - 1 ) >> 4
   k = (0010 0100 + 0001 0000 - 0000 0001) >> 4
   
   0010 0100  36
 + 0000 1111  15
 = 0011 0011  51
 
51 >> 4 = 
   0011 0011 >> 4 = 0000 0011  k = 0000 0011
   
   
第3步: slot_bytes = k << 4;
0000 0011 << 4 = 0011 0000 = 48
 

可以简单理解为:将一个数+15,然后再对结果进行左移4位,再右移4位,目的是将后4位抹零来实现十六进制对齐。有没有这样的疑问,这样一操作把后四位抹零就能实现十六进制对齐?后四位是 8 4 2 1他们全不是16的倍数,我们再看看左边的数据128 64 32 16 全都是16的倍数,并且他们任意相加也是16的倍数。是不是就必然保证了最后的结果一定是16的倍数。

上一篇有说过8进制对齐的一种算法,这里我们来对比一下:将一个数+7,然后再&~77是 0000 0111,~7 则是1111 1000,简单的可以理解为这个数头5位如果有1则进行相加,舍弃后三位,已实现8进制对齐

这两种算法都可以显示进制对齐。

sizeof()

// 可以传入一个表达式 或 数据类型 , 计算参数在内存中占用的字节数
sizeof(<#expression-or-type#>)

例如:
1. 表达式
sizeof(3+2)  结果: 4 ,因为3+2 = 5int类型,int类型占4个字节
sizeof(3==3) 结果: 1, 因为3==3true, truebool类型,占1个字节

2. 数据类型
sizeof(NSString *) 结果: 8, 因为字符串占8个字节
sizeof(char) 结果: 1
sizeof(person)     结果: 8, 因为person是一直自定义的对象,本质是结构体指针,所以占8个字节

那如何sizeof(结构体)应该怎么计算呢? 先来看下面的内存对齐。

拓展之内存对齐

  • 结构体内存对齐(成员变量内存对齐)
  • 对象内存对齐

以下是基本数据类型的占位大小

image.png

结构体内存对齐(成员变量内存对齐)

首先了解下结构体内存对齐的三大原则:

  1. 结构体的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的起始位置要从该成员大小或者成员子成员的大小(例如结构体内部套用结构体)的整数倍开始。
  2. 如果一个结构体内部有子结构体,则结构体成员要从其内部最大元素大小的整数倍开始存储。(struct a 里有struct b, b中包含了 double,int,char 则b应该从8的整数倍开始存储)。
  3. 结构体总的大小,必须是其内部最大的成员的整数倍。

例子1:

struct DXJStrunct1 {

    double a;

    char b;

    int c;

    short d;

}struct1;

NSLog(@"sizeof(struct1) = %lu",sizeof(struct1));  // 24

double a,占 8 个字节,根据结构体内存对齐第1条,则从0开始,成员大小的整数倍开始,0是8的倍数,所以占有[0-7]的位置。

char b,占 1 个字节,则占有[8]的位置。

int c, 占 4 个字节,因为要从成员大小的整数倍开始,则舍弃(9,10,11),他们都不是4的倍数,占有[12-15]的位置。

short d, 占 2 个字节,则占有[16,17]的位置。

[0 - 17] 一共18位,那sizeof(struct1)是不是占18位呢?根据结构体内存对齐原则第3条, 结构体总的大小,必须是其内部最大的成员的整数倍,所以这里18 => 24

例子2:

struct DXJStrunct3 {

    int b;

    char c; 

    short d;

    int e;

    struct DXJStrunct1 str;

}struct3;

分两部分进行内存对齐再相加,第一部分:

int b, 占 2 个字节,则占有[0-3]的位置。

char c,占 1 个字节,则占有[4]的位置。

short d,占 2 个字节,则占有[6,7]的位置。

int e,占 14个字节,则占有[8-11]的位置。

[0-11],共12个字节,最大的成员变量是DXJStrunct1 结构体内部的double,所以 12 => 16

第二部分:

struct DXJStrunct1 str ,这里计算的时候就可以从0开始计算了,因为第一部分算是已经清零了。根据例子1得出结构体str的占有内存大小为24

因为两部分分别已经进行了内存对齐,所以直接相加即可,最终的大小为16 + 24 = 40

对象内存对齐(类的内存对齐)

使用alloc创建一个对象时,经历了三步:

  • 计算类占用的内存大小(仅和成员变量有关系,如果支持编译器快速计算内存大小,则是16进制对齐,如果不支持则是8进制对齐)
  • 根据计算出来的类占用的内存大小size,调用libsystem_malloc.dylib库下的calloc(1,size)方法,对于size进行16进制对齐
  • 类(isa)和创建出来的对象进行关联

通过以上三步,可以发现,在步骤1中无论是8进制还是16进制对齐,到第二步骤时,都会进行16进制对齐。所以对象是以16进制对齐

image.png

总结

计算内存大小函数:

  • class_getInstanceSize(): 8字节对齐(x + 7) & ~7
  • malloc_size(): 16字节对齐 (x + 15) >> 4 << 4
  • sizeof(): 计算类型 / 表达式结果 占用内存的大小

内存对齐原则:

  • 结构体内存对齐: 按照结构体内存对齐三原则计算即可
  • 对象内存对齐: 影响对象的内存大小的是成员变量,开辟空间时按照16字节对齐