iOS Struct嵌套类型的内存分析

一,为什么要内存对齐

在iOS开发过程中,甚至任何一门开发语言,对于内存的资源都是极其宝贵的,不能随意的浪费,所以才会存在栈内存和堆内存的情况,栈内存就是连续的空间,由系统统一分配,而堆内存是离散的,是由程序员手动开辟使用,使用完成在进行系统回收这样的一个过程。

所以在系统为我们分配内存,然后再去读内存中相应数据的时候,如果随意存储,随意的读取相应存储的东西,这样系统的性能会大大降低,速度也是相当缓慢,从而是计算机的CPU相应的消耗过大,一直在查找、读取内存中的东西,基于这样的优化实现,所以才会有内存对齐的机制,从而使系统读取数据得到最大优化的实现。

两种内存对齐方式

对于iOS开发过程中存在两种系统内存对齐的方式

  • kind1: 16字节对齐
  • kind2: 8字节对齐

eg1: 16字节对齐方式

从objc 源码可以看出,我们系统的alloc 流程是使用的16字节对齐的方式,代码片段如下

 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;
    }

step into

size_t fastInstanceSize(size_t extra) const
    {
        ASSERT(hasFastInstanceSize(extra));

        if (__builtin_constant_p(extra) && extra == 0) {
            return _flags & FAST_CACHE_ALLOC_MASK16;
        } else {
            size_t size = _flags & FAST_CACHE_ALLOC_MASK;
            // remove the FAST_CACHE_ALLOC_DELTA16 that was added
            // by setFastInstanceSize
            return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
        }
    }

最后到 最核心的算法

static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
}

此处的原理我上一篇文章已经介绍过了,里边有很详细得位与计算,此处就不再赘述了。

eg2:

在libmalloc 开源的库里也能查到,

static void *
default_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
    zone = runtime_default_zone();
    
    return zone->calloc(zone, num_items, size);
}

step into

static inline malloc_zone_t *
inline_malloc_default_zone(void)
{
    _malloc_initialize_once();
    // malloc_report(ASL_LEVEL_INFO, "In inline_malloc_default_zone with %d %d\n", malloc_num_zones, malloc_has_debug_zone);
    return malloc_zones[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]);

最后到 也是核心算法

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;
}

这也是16对齐的一种证明,

eg 8字节对齐

在runtime源码中的 class_getInstanceSize 方法中

    // Class's ivar size rounded up to a pointer-size boundary.
    uint32_t alignedInstanceSize() const {
        return word_align(unalignedInstanceSize());
    }

step into


图片.png

会进入标注的方法,而我们看到此算法和16字节对齐的算法一样,只是16字节的mask 是15 而8字节的WORD_MASK 是7,所以足以证明是8自己对齐,这就是对象创建时候使用的内存对齐机制。好了,字节对齐应该算介绍明白了,接下来就进入今天的主题,结构体内存对齐,

二,结构体内存对齐

  • 简单的结构体内存分析

我们定义两个结构体Student 和Teacher: 两个结构体的成员都一样,只是顺序不一样,我们打印相关的内存空间发现结果是不一样的,打印结果如下

struct Student{
    long a;
    int b;
    short c;
    char d;
}struct1;
struct Tearcher{
    long a;
    short b;
    int c;
    char d;

}struct2;
图片.png

为什么是这样呢,原来这就是iOS系统中内存对齐的作用导致的,接下来我们就详细的分析结果;

在iOS系统中,所有数据类型的内存占用情况是如下:截图来自逻辑教育那个最帅的男人

图片.png

再根据内存对齐的规则

1:数据成员对⻬规则:结构(struct)(或联合(union))的数据成员,第
⼀个数据成员放在offset为0的地⽅,以后每个数据成员存储的起始位置要
从该成员⼤⼩或者成员的⼦成员⼤⼩(只要该成员有⼦成员,⽐如说是数组,
结构体等)的整数倍开始(⽐如int为4字节,则要从4的整数倍地址开始存
储。 min(当前开始的位置m n) m = 9 n = 4
9 10 11 12

2:结构体作为成员:如果⼀个结构⾥有某些结构体成员,则结构体成员要从
其内部最⼤元素⼤⼩的整数倍地址开始存储.(struct a⾥存有struct b,b
⾥有char,int ,double等元素,那b应该从8的整数倍开始存储.)

3:收尾⼯作:结构体的总⼤⼩,也就是sizeof的结果,.必须是其内部最⼤
成员的整数倍.不⾜的要补⻬。

我们从以上得知,long = 8、 int = 4、short = 2、char = 1。

struct1 的结果分析

  • a 的存储结果 是 #0 到 #7,存储8个字节,
  • b的内存开始编号为#8 ,存储4个字节,,8 又正好是4 的整数倍 , 所以 b的存储结果是#8 到 #11,
  • c 的内存开始编号是 #12,存储2个字节,12又正好是2的整数倍,所以c的存储结果是 #12到#13,
  • d的内存开始编号是 #14,存储1个字节,同理,d的存储结果就是#14

所以struct1 的内存实际分配为#0 到#14,15个,按照字节对齐,所以是打印结果是16;

struct2的结果分析

  • a 的存储结果 是 #0 到 #7,存储8个字节,
  • b的内存开始编号为#8 ,存储2个字节,,8 又正好是2 的整数倍 , 所以 b的存储结果是#8 到 #9,
  • c 的内存开始编号是 #10,存储4个字节,10不是4的整数倍,所以c的存储编号移动到 #12,所以c的存储结果是 #12到#15;
  • d的内存开始编号是 #16,存储1个字节,同理,d的存储结果就是#16

所以struct1 的内存实际分配为#0 到#16,17个,按照字节对齐,所以是打印结果是24;

大致的草图是这样的:仅个人理解,


图片.png
  • 嵌套结构体内存对齐

再次,我又在项目的两个结构体中,加入了CGPoint 的一个成员,因为CGPoint本身就是一个结构体类型。

struct Student{
    CGPoint p;
    long a;
    int b;
    short c;
    char d;

}struct1;
struct Tearcher{
    long a;
    short b;
    int c;
    char d;
    CGPoint p;

}struct2;

再次打印相关的情况如下


图片.png

从上图打印我们发现,CGFloat 的内存分配是 8,CGPoint 的类存是16,也就是坐标(x,y)的内存字节的和,

原理在简单的结构体内存对齐已经介绍了,接下来说一下我发现的问题,

问题:为什么结构体嵌套,不是作为一个整体来存储?

从上图我们知道,如果按我们简单的结构体存储,那么struct1很好理解,但是struct 这时存储的位置是17了,按照整数倍的关系的话,内存编号应该移到32才开始存储CGPoint才对,照我想象的话最后的结果应该是47才对,然而结果打印的是40,这就是验证了一个问题,即使嵌套的结构体,系统也是把每个结构体的成员拆除出来单独存储,这样才能达到这样的结果。

嵌套struct1分析
  • CGPoint 的 x 占据8个字节,存储是从#0到#7 ,存储8个字节;
  • CGPoint 的 y的内存编号是 #8,存储8个字节,#8又正好是8的整数倍,所以y的存储结果是#8到#15;
  • a 的存内存编号是 #16, 存储8个字节,#16正好有事8的整数倍,所以 a 的存储结果是 #16 到#23;
  • b的内存开始编号为#24 ,存储4个字节,,#24 又正好是4 的整数倍 , 所以 b的存储结果是#24 到 #17,
  • c 的内存开始编号是 #28,存储2个字节,#28又正好是2的整数倍,所以c的存储结果是 #28到#29,
  • d的内存开始编号是 #30,存储1个字节,同理,d的存储结果就是#30

所以嵌套struct1 的内存实际分配为#0 到#30,31个,按照字节对齐,所以是打印结果是32;

嵌套struct2分析

  • a 的存储结果 是 #0 到 #7,存储8个字节,
  • b的内存开始编号为#8 ,存储2个字节,,8 又正好是2 的整数倍 , 所以 b的存储结果是#8 到 #9,
  • c 的内存开始编号是 #10,存储4个字节,10不是4的整数倍,所以c的存储编号移动到 #12,所以c的存储结果是 #12到#15;
  • d的内存开始编号是 #16,存储1个字节,同理,d的存储结果就是#16
  • CGPoint 的 x 存储编号是#17 ,存储8个字节;#17不是8的整数倍,所以需要移动编号到#24;所以CGPoint 的 x 存储结果是#24到#31;
  • CGPoint 的 y的内存编号是 #32,存储8个字节,#32又正好是8的整数倍,所以y的存储结果是#32到#39;
    所以嵌套struct2 的内存实际分配为#0 到#39,40个,按照字节对齐,所以是打印结果是40;

总结

系统按照相关的内存对齐原则进行分配内存,嵌套类型不是作为一个整体来存储,而是吧嵌套子类型里边的类型逐个进行内存分配,这样既能节约内存消耗也能加快查询速度,通过自我学习,确实纠正了自己的一些错误观念。以后继续努力。

你可能感兴趣的:(iOS Struct嵌套类型的内存分析)