OC 底层原理(02)内存对齐

207 阅读6分钟

一. 获取内存的三种方式

1.1 sizeof

  • sizeof 是一个操作符,不是函数
  • sizeof 计算的是传进来的数据类型的大小,这个在编译时期就已经确定。
  • sizeof 最终得到的结果是该数据类型占用空间的大小

1.2 class_getInstanceSize

  • class_getInstanceSize 是runtime提供的api,用于获取 类实例对象所占内存大小,本质是计算`实例的成员变量的内存大小

1.3 malloc_size

  • malloc_size 获取系统实际分配的内存大小

1.4 代码示例

1656470727868.jpg

总结:

  • sizeof :打印出来是8字节,因为在对象的本质是 结构体指针,而指针的大小就是 8字节

  • class_getInstanceSize:对象实际的内存大小,实际内存大小是由类的成员变量的大小决定的。这里40 = isa(8字节) + name(8字节) + nickname(8字节) + age(4字节) + height(8字节),明明是36为啥输出40呢,这是因为对象内部是8字节对齐方式

  • malloc_size 是系统分配的内存大小,是按16字节对齐的方式,即分配的大小是16的倍数 ,不足16的倍数系统会自动填充字节,注意系统的16字节对齐是在实际的内存大小(经过8字节对齐)的基础上。所以这里是 48

疑问?

class_getInstanceSizemalloc_size 底层做了什么? class_getInstanceSize 怎么就是是8字节对齐的呢? 而malloc_size又怎么就是是16字节对齐的呢? 莫着急,咱继续。。。

二. 内存对齐原则

  • 数据成员的对齐规则可以理解为min(m, n) 的公式, 其中 m表示当前成员的开始位置, n表示当前成员所需要的位数。如果满足条件 m 整除 n (即 m % n == 0), nm 位置开始存储, 反之继续检查 m+1 能否整除 n, 直到可以整除, 从而就确定了当前成员的开始位置。

  • 当结构体嵌套了结构体时,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。如struct a中嵌套了 struct bb 中有 char、int、double类型元素,那么 b 应该从 8(double-8字节)的整数倍地址开始存储

  • 结构体的内存大小必须是成员中最大对齐数的整数倍,不足的需要补齐。

三. 验证对齐原则

3.1 下表是各种数据类型在ios中的占用内存大小,根据对应类型来计算结构体中内存大小

1656473605619.jpg

3.2 创建两个不同的 struct 分别验证

1656474036956.jpg

两个结构体的 成员变量是一致的,唯一不同的是成员变量的摆放顺序不同,位置不同导致所占内存大小不同,这就是内存对齐,我们按照内存对齐规则进行分析: :

Struct1分析:
  1. a1字节 从位置0开始 0%1 == 0 0的位置存a
  2. b8字节 从位置1开始 1%8 不等于0 移到8 8%8==0 8-15b
  3. c4字节 从位置16开始 16%4 等于0 16-19c
  4. d2字节 从位置20开始 20%2 等于0 20-21d
  5. 规则三,内存大小必须为结构体中最大成员8的整数倍,22 变成8的倍数 变成24
Struct2分析:
  1. b8字节 从位置0开始 0%1 == 0 7的位置存b
  2. c4字节 从位置8开始 8%4 等于0 8-11c
  3. d2字节 从位置12开始 12%4 等于0 12-13d
  4. a1字节 从位置14开始 14%1 等于0 14a
  5. 规则三,内存大小必须为结构体中最大成员8的整数倍,14变成8的整数倍 变成16

3.3 新增一个结构体 Struct3 嵌套 Struct2

1656474684135.jpg

Struct3分析:
  1. b8字节 从位置0开始 0%1 == 0 7的位置存b
  2. c4字节 从位置8开始 8%4 等于0 8-11c
  3. d2字节 从位置12开始 12%4 等于0 12-13d
  4. a1字节 从位置14开始 14%1 等于0 14a
  5. st2是一个结构体,规则二 结构体成员要从其内部最大成员大小的整数倍开始存储,而st2中最大的成员大小为8 所以st2要从8的整数倍开始,当前是从15开始 所以不符合要求,需要往后移动到16168的整数倍,符合内存对齐原则 所以 16-31st2
  6. 规则三,内存大小必须为结构体中最大成员8的整数倍,32刚刚满足

四. 内存优化

接着上面的结论,我们思考下,为什么要内存对齐,其实苹果这么做的目的让cpu读取内存的效率更高,用空间换取时间

我们创建一个类YJPerson创建一些属性,并赋值查看内存摆放是什么样的?如下:

WechatIMG208.jpeg

按照内存对齐原则,第一个为isa,第二个为name,第三个为age 后面按顺序摆放,通过po 0x0000000b00000012 打印发现 0x0000000b00000012打印出来不是 张三0x0000000100004010打印出来为张三 po 0x0000000100004030zhangsan 那age 和idnum去哪了 看到0x0000000b00000012 分开可以看出 0x0000000b 等于 idnum-11 0x00000012等于 age-18 可以看出系统进行了优化,属性进行了重排

总结:

大部分的内存都是通过固定的内存块进行读取, 尽管我们在内存中采用了内存对齐的方式,但并不是所有的内存都可以进行浪费的,苹果会自动对属性进行重排,以此来优化内存

五. 内存对齐算法

到目前为止,我们在前文既提到了8字节对齐,也提及了16字节对齐,那我们到底采用哪种字节对齐呢?

我们可以通过objc4中class_getInstanceSize的源码来进行分析

size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}
uint32_t alignedInstanceSize() const {
    return word_align(unalignedInstanceSize());
}
#define WORD_MASK 7UL
static inline uint32_t word_align(uint32_t x) {
    // x+7 & (~7) --> 8字节对齐
    // ~:按位取反,7的二进制= 0111 取反后:1000
    return (x + WORD_MASK) & ~WORD_MASK;
}

通过源码可知:

  • 对于一个对象来说,其真正的对齐方式8字节对齐,8字节对齐已经足够满足对象的需求了
  • apple系统为了防止一切的容错,采用的是16字节对齐的内存,主要是因为采用8字节对齐时,两个对象的内存会紧挨着,显得比较紧凑,而16字节比较宽松,利于苹果以后的扩展。

总结 综合前文提及的获取内存大小的方式

  • class_getInstanceSize:是采用8字节对齐,参照的对象的属性内存大小

align16: 16字节对齐算法

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