OC底层探索04-探索对象内存大小

上篇中对对象的alloc方式OC底层探索03-常用的alloc,init,new到底做了什么?进行了简单探索。在alloc时使用了一个8/16字节对齐算法来计算内存大小,想没想过为什么要这样做呢?

举例对象内存大小

        HRTest * test = [HRTest alloc];
        test.name = @"Henry";  //8字节
        test.hobby = @"woman"; //8字节
        test.height = 180.0;   //8字节
        test.a = 'a';  //1字节
        test.ab = 'A'; //1字节
        NSLog(@"\n---test类型内存大小%lu\n---HRTest实际占用内存大小%lu\n----HRTest实际内存分配大小%lu",
              sizeof(test),
              class_getInstanceSize([test class]),
              malloc_size((__bridge const void*)(test)));
  • 直接计算:size = 8 * 3 + 1 * 2 = 26(猜想)
    对象的属性大小计算是需要通过内存对齐来计算的,并不是简单的加法

  • log输出情况


出入还是非常大的,问题出在以下几点?

1. isa指针没有计算在内

/// 出自objc源码
typedef struct objc_class *Class;
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

sizeof(x)是获取当前类型的内存大小,,上文中sizeof(test)的test是一个结构体的指针,也就得到一个指针内存占用8字节

所有的类在OC中最终都会编译为objc_object(在这个问题中可以看做父类),其中包含一个isa指针,所以需要再加上8字节

size = 8 + 8 * 3 + 1 * 2 = 34(猜想)
相比较打印结果中的实际内存占用还是有一些差距。

2. class_getInstanceSize

接下来就通过源码来看看class_getInstanceSize这个函数到底是如何计算一个类的内存大小的。

size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}
uint32_t alignedInstanceSize() const {
        return word_align(unalignedInstanceSize());
}
// 8字节对齐
#   define WORD_MASK 7UL
static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}

根据源码发现在获取实际大小后,会进行8字节对齐,在下方有详细的计算过程。

size = class_getInstanceSize(34) => 40

  • 这就是一个对象实际内存占用的计算过程:iSA(指针)+ 属性大小 + 8字节对齐

3. HRTest实际内存分配大小却是48

居然这样就不得不去看看malloc_sizemalloc_size的源码是在libmalloc库里。可惜没找到对应的实现,换个角度从内存分配方法calloc(1, size)看起。

void * calloc(size_t num_items, size_t size)
{
    ...
    retval = malloc_zone_calloc(default_zone, num_items, size);
}
void * malloc_zone_calloc(...)
{
    ...
    //核心代码
    ptr = zone->calloc(zone, num_items, size);
}

这里对于zone->calloa()没法直接跟进,需要:


static void * default_zone_calloc(...)
{
    zone = runtime_default_zone();
    
    return zone->calloc(zone, num_items, size);
}
//使用相同的方式:
static void * nano_malloc(...)
{
    ...
    void *p = _nano_malloc_check_clear(nanozone, size, 0);
}
//很长但是我们的目的是找到内存大小计算的方法
static void * _nano_malloc_check_clear(...)
{
    void *ptr;
    size_t slot_key;
    size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key);
    ...
}
//终于找到了目标方法:内存计算规则
#define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM)   // 16
static MALLOC_INLINE size_t
segregated_size_to_fit(...)
{
    ...
    k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM;
    slot_bytes = k << SHIFT_NANO_QUANTUM;
    *pKey = k - 1;
    return slot_bytes;
}

在内存创建的时候系统是对实际内存占用进行了16字节对齐
40 按照16进制对齐 => 48

小结一下

当然在底层中对象的属性所占内存大小计算不简单的是做加法,而是使用了内存对齐的方法来进行计算,由于篇幅所限会在OC底层探索05-内存对齐
中对内存对齐做解释。
对象需要的实际内存需要 8字节对齐 ,而真实分配内存时又进行了16字节对齐。那么问题又来了为什么要这样做呢?

16字节对齐算法

本质就是通过位运算,将实际内存大小计算为16的倍数.8字节对齐也是类似的。

  1. 第一种方式: (x + size_t(15)) & ~size_t(15)
//拿31举例:
size(31) :            0001 1111
15 :                  0000 1111
size(31) + 15 :       0010 1110
~15          :        1111 0000
size(31) + 15 & ~15 : 0010 0000 -> 32
  1. 第二种方式: (x + size_t(15)) >>4 <<4
//拿31举例:
size(31) :            0001 1111
15 :                  0000 1111
size(31) + 15 :       0010 1110
   >> 4:              0000 0010
   << 4:              0010 0000 -> 32

字节对齐的优势:

  • CPU把内存当成是一块一块的,块的大小可以是2,4,8,16字节大小,因此CPU在读取内存时是一块一块进行读取的。每次内存存取都会产生一个固定的开销,减少内存存取次数将提升程序的性能。

  • 16字节对齐后,可以加快CPU读取速度,同时使访问更安全,不会产生访问混乱的情况

早期的iOS系统中对象内存大小计算是通过8字节对齐,在分配内存时又进行了16字节对齐;而现在iOS系统中对象的内存大小计算是直接进行16字节对齐,通过这种方式来进一步优化对象的创建流程。

你可能感兴趣的:(OC底层探索04-探索对象内存大小)