iOS底层之isa指针分析,calloc分析和内存对齐(2)

上一篇大概阐述了alloc流程,也了解到alloc是如何创建对象,分配内存的。在最后提出了3个问题,那今天这篇就是描述上一篇所留的问题。

1.isa指针

脱离源码来说,那叫耍流氓。所以本文还是以源码来进行展开。

上一篇我们可以看到isa.cls = cls;那这个isa到底是何方神圣呢

点击isa进去,是一个isa_t isa,那我们再点isa_t

union isa_t {

    isa_t() { }

    isa_t(uintptr_t value) : bits(value) { }

    Class cls;

    uintptr_t bits;

#if defined(ISA_BITFIELD)

    **struct** {

         ISA_BITFIELD;  // defined in isa.h

    };

#endif

};
复制代码

是一个联合体。意味着共有内存。

从上面可以看到有属性cls。当然,不然怎么又isa.cls = cls对吧。

我们可以发现,里面有一个结构体。

点击ISA_BITFIELD


      uintptr_t has_assoc         : 1;                                       \

      uintptr_t has_cxx_dtor      : 1;                                       \

      uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \

      uintptr_t magic             : 6;                                       \

      uintptr_t weakly_referenced : 1;                                       \

      uintptr_t deallocating      : 1;                                       \

      uintptr_t has_sidetable_rc  : 1;                                       \

      uintptr_t extra_rc          : 19
复制代码

这些是什么呢,在arm64下,它们加起来既然是64。想到什么了吗

是位域。isa指针有8个字节,共64位二进制。

举个例子,我们要存一个false和true的结果,我们需要用到8个字节吗,0b0000 0000和0b0000 0001,实际上我们是不是用1位就能表示结果了。同样这里也一样的道理。

这样的好处是为了节省存储空间,处理也简便。

那上面的是什么意思呢

1.1.位域

属性 大小 作用
nonpointer 1 是否对isa指针开启指针优化。0为纯isa指针,1位包含类信息,对象引用计数等
has_assoc 1 关联对象标记位,0为没有,1为存在
has_cxx_dtor 1 对象是否有C++或者Objc析构函数
shiftcls 33 存储类指针的值。
magic 6 用于调试器判断当前对象是否已初始化
weakly_referenced 1 对象是否被指向或者曾经指向一个ARC的弱变量
deallocating 1 标记对象是否正在释放内存
has_sidetable_rc 1 当对象引用计数大于10时,用来存储进位
extra_rc 19 对象实际的引用计数值减1

2.calloc

calloc分析就要用到libmalloc库来分析了,这也就是为什么我们上一篇calloc点击进去,看不到具体的内容的原因。

2.1.calloc方法

我们直接看源码


calloc(size_t num_items, size_t size)

{

**void** *retval;

retval = malloc_zone_calloc(default_zone, num_items, size);

**if** (retval == **NULL**) {

errno = ENOMEM;

}

**return** retval;

}
复制代码

我们这里的size是传了对象申请内存大小,上一篇已经阐述了是8字节对齐。

这里我们可以看到重点是调用了malloc_zone_calloc

2.2.malloc_zone_calloc


malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)

{
    MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);
    void *ptr;
    if (malloc_check_start && (malloc_check_counter++ >= malloc_check_start)) {

    internal_check();

    }
    ptr = zone->calloc(zone, num_items, size);

    if (malloc_logger) {

    malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE | MALLOC_LOG_TYPE_CLEARED, (uintptr_t)zone,

    (uintptr_t)(num_items * size), 0, (uintptr_t)ptr, 0);

    }

    MALLOC_TRACE(TRACE_calloc | DBG_FUNC_END, (uintptr_t)zone, num_items, size, (uintptr_t)ptr);

    return** ptr;
}
复制代码

这里的重点是返回ptr,那我们可以知道关键是ptr = zone->calloc

我们点击进去

    struct _malloc_zone_t *zone, size_t num_items, size_t size); /* same as malloc, but block returned is set to zero
复制代码

显示的看不懂啊,完了,看不了。怎么办,结束了吗。

其实想看也是可以的,这是一个属性函数,我们直接lldb指令下打印一下方法,就会看到指向函数显示defaut_zone_calloc

我们全局搜索一下


{

zone = runtime_default_zone();

return zone->calloc(zone, num_items, size);

}
复制代码

又是zone->calloc,我们继续lldb打印一下。显示nano_calloc

static** void * nano_calloc(nanozone_t *nanozone, size_t num_items, size_t size)
{
    size_t total_bytes;
    if (calloc_get_size(num_items, size, 0, &total_bytes)) {
        return** **NULL**;
    }
    if (total_bytes <= NANO_MAX_SIZE) {

        void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);

        if (p) {

            return p;

        } else {
            /* FALLTHROUGH to helper zone */
        }
    }

    malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone);

    return zone->calloc(zone, 1, total_bytes);

}
复制代码

我们直接找到重p = _nan_malloc_check_clear。点击进去看看。

2.3._nano_malloc_check_clear

static void * _nano_malloc_check_clear(nanozone_t *nanozone, size_t size, boolean_t cleared_requested)
{
    MALLOC_TRACE(TRACE_nano_malloc, (uintptr_t)nanozone, size, cleared_requested, 0);
    **void** *ptr;
    size_t slot_key;
    size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); // Note slot_key is set here
    mag_index_t mag_index = nano_mag_index(nanozone);
    nano_meta_admin_t pMeta = &(nanozone->meta_data[mag_index][slot_key]);
    ptr = segregated_next_block(nanozone, pMeta, slot_bytes, mag_index);
    return ptr;
}
复制代码

源码方面,我这里删减了很多,因为很多代码都是判断错误处理的,我就去掉,我们主要捉住核心就好segregated_next_block

之前我们前面方法,都是size,这次变成了slot_bytes,说明我们这个内存大小进行处理了,那我们就很容易找到segregated_size_to_fit这个方法了。

2.4.segregated_size_to_fit

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;

}
复制代码

重点来了,代码是不是有点和申请内容字节对齐相似。

Snipaste_2021-12-09_11-27-02.png

我们看下是怎么算的,传进来的size 是16,NANO_REGIME_QUANTA_SIZE也是16,SHIFT_NANO_QUANTUM为4,那就变成了16 + 16 - 1 右移4位然后左移4位。

得出31 >> 4 << 4。按上图最后得出16。

那我们可以得出一个结论,系统分配内存大小为16字节

3.内存对齐

由上一篇alloc分析,到这一篇分析,我们终于知道了对象占用大小为8字节对齐,系统分配内存大小为16字节对齐。

但是,这个是怎么算的呢,何为8字节对齐,何为16字节对齐,8字节对齐时候,属性又是怎么排序的呢,内存又是怎么优化的呢。

3.1.数据成员对齐规则

struct和union的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的存储的起始位置要从该成员大小或者成员的子成员大小的整数倍开始。

3.2.结构体作为成员

如果一个结构里有结构体成员,则其结构体成员要从其内部最大元素大小的整数倍地址开始存储。

3.3.结构体总大小

总大小,必须是其内部最大成员的整数倍,不足要补齐。

3.4.内存数据例子

文字类不太懂是吧,那我们就举个例子。

Snipaste_2021-12-09_17-10-28.png

先放一张基本类型的占用大小图。下面我们看例子

struct Struct1 {

    char a;     // 0

    double b;  // 8~15

    int c;     // 16~19

    short d;   // 20~21

} Struct1;
复制代码

char为1,double为8,int为4,short为2。

offset从0开始,所以a为0。

由3.1我们知道必须整数倍开始,b为8,所以必须到第8位开始,则占位置为8~15。

c为4,刚好16为整数倍,所以站位为 16 ~ 19。d为2,故为20 ~ 21。

由3.3,我们知道内部大小需为成员整数倍,不足补齐,所以需要以8字节对齐,故分配内存为24个字节。

系统是有sizeof方法,可以来验证大小的。

可能你们有这样的想法,为什么不连续排,这样不就更省空间了吗。原因是为了更方便快速查找,用空间换时间,增加效率了

4.内存大小

我们创建一个类,类有多种属性,那我们怎么知道对象要占用多大,系统分配多大。

4.1. class_getInstanceSize

size_t class_getInstanceSize(Class cls)

{
    if (!cls) return 0;

    return cls->alignedInstanceSize();
}
复制代码

通过class_getInstanceSize,我们就知道对象要占用多大了。

4.2. malloc_size

    malloc_size
复制代码

通过 malloc_size方法,我们需要桥接一下,就知道系统要分配多大了。

5.总结

  1. isa指针为8字节,共64位,里面存放着指针优化,引用计数,释放,弱指针,c++,关联对象等信息。
  2. calloc系统为对象分配内存,以16字节对齐分配。
  3. 通过内存对齐方式,来了解系统是如何分配的。

最后,按照国际惯例,提出几个问题

  1. 对象和类明显是有指向的,那它们的具体联系是什么?
  2. 对象可以创建多个,类也可以有多个吗
  3. 对象的本质是什么?

这些问题,下一篇文章就会说到,敬请期待吧。

猜你喜欢

转载自juejin.im/post/7039633278302683149