OC对象底层内存开辟和实现(下)

1,687 阅读7分钟

上一节中我们探索一下ObjcLLVM源码底层关于alloc的实现过程,并了解了如何来标识记录类的各种状态,总结并解释了提出的相关问题,这节我们继续看下alloc后续关于内存开辟、内存对齐以及类绑定的问题。

一、影响对象内存大小的因素

在alloc执行后,代码会调用_objc_rootAllocWithZone->_class_createInstanceFromZone来进行内存空间的开辟,我们在源码中先输出一下对象的内存大小和对象在系统中实际开辟的内存大小

1、LGPerson类没有属性和方法时

初始情况下内存大小的情况 image.png 初始时,默认只有isa占用8个字节;(对象实际大小时按照16字节对齐,下面讲解),所以输出 8 、8、16;

2、LGPerson类声明四个属性时

image.png image.png 属性占用字节分别为1、4、8、8,还有默认的isa 8个字节,根据结构体内存对齐原则(基础知识应该很熟吧),输出分别为8、32、32;

添加属性时,成员变量的大小和系统开辟的大小都增加了

3、LGPerson类属性换成成员变量时

image.png image.png 和属性输出是一样的,如果把成员变量的顺序换一下

4、LGPerson类中成员变量的顺序调换

image.png image.png 神奇的一幕出现了,调换成员变量的顺序后,成员变量的大小系统开辟的大小都发生了变化,输出变成了8、40、48;

这是因为根据结构体中顺序决定了内存对齐,(而实际的对象大小是以16字节对齐返回)。

5、LGerson类调换属性的顺序并添加两个方法

image.png image.png 改变属性顺序和添加方法,都没有对内存的大小产生影响

我们从上面5种情况我们可以推断出影响对象内存大小的因素是: 属性(或者成员变量)的个数成员变量的顺序;那接下来我们分析一下源码是如何实现的。

函数说明

  • sizeof 计算数据类型或者指针大小,p指针为8字节;
  • class_getInstanceSize类中成员变量的大小,在类结构体class_ro_t中instanceSize字段来记录(后面详细分析);
  • malloc_size计算实际向系统申请开辟的内存空间大小,遵循16字节对齐原则;

小结: 系统底层针对属性做了内存对齐的优化,无论属性顺序如何变化,都会按照最优的对齐方式对齐; 而成员变量的对齐并不会进行优化处理;

所以日常开发中我们尽量使用属性,不直接使用成员变量,节约内存;

二、_class_createInstanceFromZone源码分析

源码如下:

image.png 仔细分析上面源码,总结就是干了3件事:

  1. instanceSize计算成员变量的内存大小;
  2. calloc/malloc_zone_calloc 开辟内存空间大小(这个空间就是实例对象的空间大小)(默认执行calloc,因为参数默认传nil);
  3. initIsa/initInstanceIsa 关联isa和class;(默认执行initInstanceIsa,fast标识是否需要开启nonpointter,实例对象是isa_t联合体,类对象是isa指针) 接下来我们来看具体实现:

三、instanceSize内存计算

1、instanceSize方法

image.png 底层代码会读取cache(缓存)中的fastInstaceSize大小(存储在cache的_flags中,在上节中我们说过cache中的_flags第3-12位来存储instanceSize大小);

因为在编译阶段成员变量大小已经确定,存放在MachO文件中;当类加载时,会在realizeClassWithoutSwift->reconcileInstanceVariables计算父类的成员变量的大小,并修改class_ro_t中的起始大小(默认isa指针大小)和instanceSize大小;

在老版本的源代码中,是没有缓存的,在instanceSize方法中执行的是 读取缓存下面的代码。

size_t size = alignedInstanceSize() + extraBytes;

if (size < 16) size = 16;

return size; alignedInstanceSize()方法中word_align方法是8字节对齐算法(如上图标注部分);

2、cache.fastInstaceSize方法

image.png 读取_flags中缓存的成员变量的大小,【 在上节中我们说过cache中的_flags第3-12位来存储instanceSize大小,最小可存储8字节(1000),最大存储4M(1 0000 0000 0000, 1024*1024 *4字节)】,align16方法就是16字节对齐算法,所以对象的实际大小,以16字节对齐返回

下图 是类加载时如何缓存instanceSize大小:

realizeClassWithoutSwift方法中 image.png 加上父类的instanceSize大小后,调用setinstanceSize方法缓存记录;

reconcileInstanceVariables方法中: image.png 如果父类的成员变量大小 大于 起始的8个字节(所有对象都有默认的isa起始8个字节),则说明父类有其它的成员变量,调用moveIvars修改成员变量内存大小;

image.png 计算父类的成员变量大小,首先要减去起始默认的isa的字节(所有对象都有默认的isa起始8个字节);然后再8字节对齐;最后修改起始字节数和instanceSize大小;

小结:

在获取instanceSize大小时,成员变量大小(排序有关)都是以8字节对齐的;

而实际返回的对象大小是以16字节对齐的;

一个对象的占用内存大小 最小为8字节,最大为4M;

关于字节对齐word_align和align16算法???

四、calloc/malloc_zone_calloc内存开辟

id obj;

if (zone) {

   obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);

} else {

    obj = (id)calloc(1, size);

}

在开辟内存空间时,size大小就是返回对象的实际大小(以16字节对齐),我们通过跳转定义看到calloc/malloc_zone_calloc源码在malloc库中 image.png 我们在苹果的开发社区看到(malloc下载地址)

image.png 最新的源码版本为317.40.8,我们下载探究一下: image.png 在源码中calloc 其实就是调用的是malloc_zone_calloc方法,传入了一个默认的default_zone参数(初始化的一个malloc_zone_t结构体);

image.png 中间查找过程省略。。。 直接贴结果函数_nano_malloc_check_clear image.pngsegregated_size_to_fit对开辟的空间大小再次用16字节对齐验证;代码如下: image.png 宏定义NANO_REGIME_QUANTA_SIZE为16,SHIFT_NANO_QUANTUM为4,算法公式为(32+(16-1))>> 4 << 4, 此公式为16字节对齐计算公式。

五、initIsa/initInstanceIsa关联isa

分别看下initInstanceIsainitIsa底层代码 image.png image.png 可以看到底层都是调用了initIsa函数只不过参数不同而已;第一个参数为当前类,第二个参数为isa是否开启nonpointer(类对象的isa为指针,实例对象的isa为isa_t联合体),第三个参数为是否有默认的.cxx_destruct 析构方法的实现。

image.png 初始化isa,当nonpointer为flase时,说明不需要开启nonpointer优化,那么isa指针直接指向了class类;

当nonpointer为true时,说明需要开启nonpointer优化,那么给isa_t位域赋值并指向class类;然后给objc_object(对象)中的isa赋值绑定;

最后开辟的空间对象obj指向了初始化的对象,完成和类的关联;

六、疑问解答

1、class_getInstanceSize函数

源码如下:

size_t class_getInstanceSize(Class cls)

{

    if (!cls) return 0;

    return cls->alignedInstanceSize();

}

uint32_t alignedInstanceSize() const {

     return word_align(unalignedInstanceSize());

}

uint32_t unalignedInstanceSize() const {

        ASSERT(isRealized());

        return data()->ro()->instanceSize;

}

static inline uint32_t word_align(uint32_t x) {

    return (x + WORD_MASK) & ~WORD_MASK;

}

看到这个 alignedInstanceSize方法有没有很熟悉,方法中word_align方法是对获取的instanceSize大小以8字节对齐;获取的是对象中成员变量占用的内存大小,如果此时内存大小恰好也是16字节对齐,那么获取的instanceSize也是对象的大小。

2、字节对齐

word_alignalign16字节对齐算法,其中以8字节对齐为例,用两种算法公式来说明:

  1. (size + 7) >> 3 << 3

假设 size 等于6, 6 + 7 = 13 ==>二进制为 0000 1101

>>3 ==> 二进制为 0000 0001<<3 ==> 二进制为 0000 1000

转换为10机制就是 8

算法核心是 把二进制的最后三位置为0

  1. (size + 7) & ~7

假设size等于1010 + 7 = 170001 0001

~77取反,1111 1000

0001 0001 & 1111 1000 = 0001 0000

转换10进制16

算法核心同样是 把二进制的最后三位置为0

总结:

  • 对象中成员变量(结构体内部)采用8字节对齐;
  • 对象与对象在堆内存中采用16字节对齐;
  • 系统会对属性进行内存对齐优化,尽量不直接使用成员变量;
  • malloc_size获取对象实际开辟了内存空间大小;