iOS 底层原理 -内存对齐、calloc及isa初探

189 阅读9分钟

一、calloc 底层探索

1.1 探索之前先了解一下内存对齐三大原则

  • 数据成员对齐原则: 结构( struct )(或联合( union ))的数据成员,第一个数据成员放在 offset 为 0 的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小的倍数开始;

例如:int 类型是4个字节,从4的整数倍地址开始存储

  • 结构体作为成员: 如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储ds
struct Struct1 {
    double a;  //8字节    
    char b;    //1字节     
    int c;     //4字节     
    short d;   //2字节      
}struct1;

如上代码结构struct1应该从8的整数倍开始存储,因为double占用8个字节

  • 收尾工作: 结构体的总大小,也就是 sizeof 的结果必须是其内部最大成员的整数倍.不足的要补⻬。

如下例子

// 结构体内存对齐
struct Struct1 {
    double a;       // 8字节    [0 7]
    char b;         // 1字节   [8]
    int c;          // 4字节    (9 10 11 [12 13 14 15] 
                    //int从12开始原因是必须是4字节倍数,9不是,下面同理
    short d;        // 2字节    [16 17]
    //其总大小为[0 17] = 3 * 8 ==24,因为sizeof的结果必须是其内部最大成员的整数倍,不足的要补⻬,最大为8字节
}struct1;
struct Struct2 {
    double a;       // 8字节    [0 7]
    int b;          // 4字节    [8 9 10 11]
    char c;         // 1字节   [12]
    short d;        // 2字节    (13 [14 15]
    //其总大小为[0 15]为16,因为sizeof的结果必须是其内部最大成员的整数倍,不足的要补⻬,最大为8字节
}struct2;
struct Struct3 {
    double a;       // 8    [0 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  
    //其总大小为[0 19] = 3 * 8 ==24,因为sizeof的结果必须是其内部最大成员的整数倍,不足的要补⻬,最大为8字节
//    struct Struct1 str; //24
}struct3;

如果我们修改一下Struct3呢

struct Struct3 {
    double a;       // 8    [0 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 Struct1 str; //24字节 开始从20存储不是8倍数所以从(20,21,22,23) 存放在[24 47]之间,[0 47]大小为48,正好是double8倍数,所以大小为48;
}struct3;

我们输出:NSLog(@"\n第一个结构体大小:%lu\n第二个结构体大小:%lu\n第三个结构体大小:%lu", sizeof(struct1), sizeof(struct2), sizeof(struct3));

打印结果如下:

第一个结构体大小:24
第二个结构体大小:16
第三个结构体大小:48
为什么内存对齐
struct Struct1 {
    double a;       // 8字节    [0 7]
    char b;         // 1字节   [8]
    int c;          // 4字节    (9 10 11 [12 13 14 15] 
                    //int从12开始原因是必须是4字节倍数,9不是,下面同理
    short d;        // 2字节    [16 17]
    //其总大小为[0 17] = 3 * 8 ==24,因为sizeof的结果必须是其内部最大成员的整数倍,不足的要补⻬,最大为8字节
}struct1;

看到结构体中 int c 是从12的位置开始存储4个字节 cpu在存取数据时,并不是以字节为单位,而是以块为单位存取,当读取到下面这段特殊的组合内存区域时,8字节读取肯定是无法解析的,而如果以1字节为单位进行读取会极大的影响性能,效率太低!

在读取这段特殊的组合内存区域时,根据已知的数据组合,里面最大的4个字节空间去读,需要读取两次,并且需要将两次的数据拼接后才能获取int。 在这里插入图片描述 在这里插入图片描述

1.2 对象申请内存和系统开辟内存

通过以下代码:

    Person *person = [[Person alloc]init];//isa 8字节
        person.name      = @"name";     //8字节
        person.nickName  = @"nickName"; //8字节
        person.age = 12;   //int          4字节
        person.height = 14;//long         8字节
        NSLog(@"sizeof ========= %lu",sizeof(person));
        NSLog(@"对象自己申请的内存大小 ==== %lu",class_getInstanceSize([Person class]));
        NSLog(@"系统开辟内存 ======= %lu",malloc_size((__bridge const void *)(person)));

输出结果: 2021-07-18 22:15:44.052676+0800 002-系统内存开辟分析[77161:8471600] sizeof ========= 8 2021-07-18 22:15:44.053338+0800 002-系统内存开辟分析[77161:8471600] 对象自己申请的内存大小 ==== 40 2021-07-18 22:15:44.053431+0800 002-系统内存开辟分析[77161:8471600] 系统开辟内存 ======= 48

why? 这里我们知道对象申请的内存大小是 40 个字节,而系统开辟的是 48 个字节。

结果分析

  1. sizeof: 是一个运算符,获取的是类型的大小(int、size_t、结构体、指针变量等)——>perple结构体指针类型,所以返回8字节;

  2. **class_getInstanceSize:**是一个函数(调用时需要开辟额外的内存空间),程序运行时才获取,计算的是类的大小(至少需要的大小)——>属性、成员变量

    • 创建的对象【至少】需要的内存大小
    • 其中class_getInstanceSize,源码核心(x + WORD_MASK) & ~WORD_MASK;8字节对齐
    • 不考虑malloc函数的话,内存对齐一般是以【8】对齐
    • #import <objc/runtime.h>
  3. **malloc_size:**堆空间【实际】分配给对象的内存大小——>16字节对齐

    • malloc函数分配的内存大小总是【16】的倍数;
    • #import <malloc/malloc.h>

40 个字节不难理解,是因为当前对象有 4 个属性,有三个属性为 8 个字节,有一个属性为 4个字节,再加上 isa 的 8 个字节,就是 32 + 4 = 36 个字节,然后根据内存对齐原则,36 不能被 8 整除,36 往后移动刚好到了 40 就是 8 的倍数,所以内存大小为 40。

接下来我们分析class_getInstanceSize和malloc_size(其在malloc)的底层

1.2.1class_getInstanceSize
size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}

成员变量class_getInstanceSize会输出 8 个字节对齐

1.2.2 malloc_size

从上一篇文章中我们了解到,calloc方法作为alloc流程的核心方法之一,其主要功能是为对象分配内存空间,并且返回指向该内存地址的指针,而且class_getInstanceSize里计算出了对象需要申请的内存大小,那系统为对象实际分配的内存大小和需要申请的内存大小是一样吗?

calloc分析

在这里插入图片描述 在这里插入图片描述 calloc 我们知道不在objc系统库中,需要malloc系统库

准备一份libmalloc源码,运行一下代码:

void *p = calloc(1, 24); NSLog(@"%lu", malloc_size( p));

申请40,按照16字节对齐原则,实际开辟控件应该是32字节!也就是说malloc_size(p)应该等于48。 在这里插入图片描述 运行结果是48,我们可以来到源码分析一下:

_malloc_zone_calloc 在这里插入图片描述 我们继续运行代码进入calloc只得到函数声明,往下不知道怎么进行了?在这里插入图片描述 我们知道有函数声明必然有函数实现,此时我们输出一下:

在这里插入图片描述 全局搜索default_zone_calloc进入如下函数: 在这里插入图片描述 同上操作我需要找到其实现函数nano_calloc函数 在这里插入图片描述 进入_nano_malloc_check_clear函数,此函数中我们主要关注点是内存大小所以我们可以直接定位到标记位置 在这里插入图片描述 在这里插入图片描述 进入segregated_size_to_fit函数 在这里插入图片描述 解析

在这个方法里找到了16字节对齐算法,也就是系统实际开辟的内存空间的大小。其中NANO_REGIME_QUANTA_SIZE = 16,SHIFT_NANO_QUANTUM = 4,所以k = (24 + 16 - 15) >> 4;之后再左移4位,16字节对齐!

内存申请和内存分配总结

alloc创建对象时,系统会先经过instanceSize方法计算需要申请多大的内存空间(已默认16字节对齐),再在calloc方法里对申请的内存大小进行16字节对齐处理,然后按处理后的结果来给对象分配内存空间,并返回内存地址。系统最终实际为对象分配的内存空间大小为16字节的整数倍,并且最少16字节,如果instanceSize方法里是按16字节对齐的,那实际分配的内存大小和申请的内存大小相同;如果是按8字节对齐,则不同。

_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
                              int construct_flags = OBJECT_CONSTRUCT_NONE,
                              bool cxxConstruct = true,
                              size_t *outAllocatedSize = nil)
{
    ASSERT(cls->isRealized());

    // Read class's info bits all at once for performance
    bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();
    size_t size;

    size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (zone) {
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        obj = (id)calloc(1, size);
    }
    if (slowpath(!obj)) {
        if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
            return _objc_callBadAllocHandler(cls);
        }
        return nil;
    }

    if (!zone && fast) {
        obj->initInstanceIsa(cls, hasCxxDtor);
    } else {
        // Use raw pointer isa on the assumption that they might be
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

    if (fastpath(!hasCxxCtor)) {
        return obj;
    }

    construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
    return object_cxxConstructFromClass(obj, cls, construct_flags);
}

从上面代码可知:在调用calloc之前需要得到size

    if (zone) {
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        obj = (id)calloc(1, size);
    }

获取size:源码

size = cls->instanceSize(extraBytes);
对应的源码:
inline size_t instanceSize(size_t extraBytes) const {
        if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
            return cache.fastInstanceSize(extraBytes);
        }

        size_t size = alignedInstanceSize() + extraBytes;
        // CF requires all objects be at least 16 bytes.
        if (size < 16) size = 16;
        return size;
    }

可知其至少是16字节的输出;

二、isa 底层探索

2.1、了解联合体和位域

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }
    uintptr_t bits;
    Class cls;

public:
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  
    };

    bool isDeallocating() {
        return extra_rc == 0 && has_sidetable_rc == 0;
    }
    void setDeallocating() {
        extra_rc = 0;
        has_sidetable_rc = 0;
    }
#endif

    void setClass(Class cls, objc_object *obj);
    Class getClass(bool authenticated);
    Class getDecodedClass(bool authenticated);
};

在探索isa时候,会发现 isa 其实是一个联合体,达到了内存优化的效果,因为联合体是所有成员共享一个内存,联合体内存的大小取决于内部成员内存大小最大的那个元素,对于 isa 指针来说,就不用额外声明很多的属性,直接在内部的 ISA_BITFIELD 保存信息,联合体属性间是互斥的。

需要补充联合体和位域

2.2、isa结构

isa作为联合体,其内部有结构体ISA_BITFIELD属性,知道其占8个字节,二进制64位

在arm64架构下其代码如下:

#     define ISA_MASK        0x0000000ffffffff8ULL
#     define ISA_MAGIC_MASK  0x000003f000000001ULL
#     define ISA_MAGIC_VALUE 0x000001a000000001ULL
#     define ISA_HAS_CXX_DTOR_BIT 1

#     define ISA_BITFIELD                                                      
        uintptr_t nonpointer        : 1;                                       
        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 unused            : 1;                                       
        uintptr_t has_sidetable_rc  : 1;                                       
        uintptr_t extra_rc          : 19
#     define RC_ONE   (1ULL<<45)
#     define RC_HALF  (1ULL<<18)
  • nonpointer: 表示是否对 isa 指针开启指针优化;

    • 0: 纯 isa 指针
    • 1: 不止是类对象地址, isa 中包含了类信息、对象的引用计数等
  • has_assoc: 关联对象标志位,0 没有,1 存在;

  • has_cxx_dtor: 该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象;

  • shiftcls: 存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针;

  • magic: 用于调试器判断当前对象是真的对象还是没有初始化的空间;

  • weakly_referenced: 标志对象是否被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放;

  • deallocating: 标志对象是否正在释放内存;

  • has_sidetable_rc: 当对象引用计数大于 20 时,则需要借用该变量存储进位;

  • extra_rc: 当表示该对象的引用计数值,实际上是引用计数值减 1,例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 20,则需要使用到下面has_sidetable_rc。

未完待续