OC内存对齐原理

一、获取内存大小的三种方式

1.1 sizeOf
  • sizeof是一个操作符,不是函数
  • sizeof计算内存大小时,传入的主要是对象或者数据类型,在编译器的编译阶段大小就已经确定
  • sizeof计算出的结果,是表示占用空间的大小
1.2 class_getInstanceSize
  • class_getInstanceSize runtime 提供的api,用于获取类实例对象所占内存大小,本质也是计算该实例中所有成员变量的内存大小
1.3 malloc_size
  • malloc_size获取系统实际分配的内存大小
1.4 code示例
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@end

#import <Foundation/Foundation.h>
#import "LGPerson.h"
#import <objc/runtime.h>
#import <malloc/malloc.h>
int main(int argc, const char * argv[]) {
   
    LGPerson *p = [LGPerson alloc];
    NSLog(@"p对象类型占用的内存大小:%lu",sizeof(p));
    NSLog(@"p对象实际占用的内存大小:%lu",class_getInstanceSize([p class]));
    NSLog(@"p对象实际分配的内存大小:%lu",malloc_size((__bridge const void*)(p)));
    return 0;
}

复制代码

打印结果如下:

image.png

总结
  • sizeof 打印的p为对象指针地址 一个指针的大小为8字节
  • class_getInstanceSize 打印的是内部成员变量的大小 isa8字节 name字符串8字节 age int类型 4字节 8+8+4=20 因为类的成员变量按照8字节对齐规则 20->24
  • malloc_size 为系统实际分配的大小,分配给对象的大小是按照16字节对齐,16字节对齐的算法 简单理解为 24+(16 -1 )>>4 <<4 得到 32

二、内存对齐规则

  • 【原则一】 数据成员的对齐规则可以理解为min(m, n) 的公式, 其中 m表示当前成员的开始位置, n表示当前成员所需要的位数。如果满足条件 m 整除 n (即 m % n == 0), nm 位置开始存储, 反之继续检查 m+1 能否整除 n, 直到可以整除, 从而就确定了当前成员的开始位置。
  • 【原则二】数据成员为结构体:当结构体嵌套了结构体时,作为数据成员的结构体的自身长度作为外部结构体的最大成员的内存大小,比如结构体a嵌套结构体bb中有charintdouble等,则b的自身长度为8
  • 【原则三】最后结构体的内存大小必须是结构体中最大成员内存大小的整数倍,不足的需要补齐。

三、验证规则

1.1 各数据类型所占字节大小

image.png

1.2 创建两个不同的结构体 myStuct1,myStruct2 分别进行验证 code如下
struct myStruct1 {
    char a; // 1
    double b; //8
    int c; //4
    short d; //2
}myStruct1;
struct myStruct2 {
    double b;//8
    int c;//4
    short d;//2
    char a;//1
}myStruct2;
NSLog(@"%lu-%lu",sizeof(myStruct1),sizeof(myStruct2));
复制代码

打印结果如下:24-16

image.png

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

分析结构体myStruct1
  • 【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
分析结构体myStruct2
  • 【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
1.3 新增一个结构体myStruct3 嵌套myStrct2 code如下
struct myStruct2 {
    double b;//8
    int c;//4
    short d;//2
    char a;//1
}myStruct2;

struct myStruct3 {
    double b;//8
    int c;//4
    short d;//2
    char a;//1
    struct myStruct2 str;
}myStruct3;
NSLog(@"%lu",sizeof(myStruct3));
复制代码

打印结果如下:32

image.png

分析结构体myStruct3
  • 【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】str是一个结构体,规则二 结构体成员要从其内部最大成员大小的整数倍开始存储,而myStruct2中最大的成员大小为8 所以str要从8的整数倍开始,当前是从15开始 所以不符合要求,需要往后移动到16168的整数倍,符合内存对齐原则 所以 16-31str
  • 【5】规则三,内存大小必须为结构体中最大成员8的整数倍,32刚刚满足

四、内存优化

思考内存优化

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

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

@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic,assign)int height;
@end
LGPerson *p = [LGPerson alloc];
p.name=@"lb";
p.nickName=@"LB";
p.age=18;
p.height=180;
复制代码

断点查看p对象的内存地址

image.png

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

总结

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

五、为什么分配内存大于实际占用内存

通过上面的探索,我们知道对象的内存是根据你成员变量内存大小通过8字节对齐来计算的 但是实际内存分配大小不是按照8字节来的,是按照16字节对齐来的 我们可以通过objc源码来探究 code如下

通过 size = cls->instanceSize(extraBytes); //计算大小

//alignedInstanceSize() + 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;
}
uint32_t alignedInstanceSize() const {
        return word_align(unalignedInstanceSize());
}
static inline uint32_t word_align(uint32_t x) {
//x+7 & (~7) --> 8字节对齐
    return (x + WORD_MASK) & ~WORD_MASK;
}
#   define WORD_MASK 7UL
复制代码

通过源码可以知道,计算内存大小实际是按照8字节对齐计算的

  • class_getInstanceSize:是采用8字节对齐,参照的对象的属性内存大小
  • malloc_size:采用16字节对齐,参照的整个对象的内存大小,对象实际分配的内存大小必须是16的整数倍

总结:

  • 对象大小和成员变量的类型和数量有关
  • 对象实际内存是按照8字节对齐
  • 对象分配内存大小按照16字节对齐。
  • 16字节对齐方法,内存占用大小x x+15 >>4 <<4 得到内存分配大小

ps:逻辑教育牛逼

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享