从结构体内存对齐到OC对象内存对齐

1、结构体内存对齐

结构体对齐规则:

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的结果,.必须是其内部最⼤成员的整数倍.不⾜的要补⻬。

根据上面规则,我们来进行几道练习题:

struct LGStruct1 {
    int a;      // [0-3]
    char b;        // [4]
    int c;         // 5 6 7 [8 9 10 11]
    short d;       // [12 13]
}struct1;
//内部最大是int,也就是4位,所以需要4字节对齐, struct1就是16字节

//同理:struct2的内存对齐计算
struct LGStruct2 {
    int a;           // [0-3]
    int b;         // [4 - 7]
    char c;        // [8]
    short d;       // 9  [10 11]
}struct2;
//内部最大是int,也就是4位,所以需要4字节对齐, struct1就是12字节

//同理:struct3的内存对齐计算
struct LGStruct3 {
    
    double a;    // [0-7]
    int e;       // [8 9 10 11]
    struct LGStruct1 str;  //最大元素是int,所以相当于4位倍数开始存储,[12 - 28]
}struct3;
//内部最大元素是double,所以需要8字节对齐, struct3 就是 32字节

struct LGStruct4 {
    char a;          // [0]
    struct LGStruct3 str;  //最大元素是double,8的倍数开始存储,[8 - 39]
}struct4;
//内部最大元素是LGStruct3,其内部最大是8位, struct4 就是 40字节

做完上面练习题,对结构体的内存规则应该是比较熟悉了,这里发现了一个奇怪的现象,也就是struct1和struct2存储的内容是一样多的,但是大小却不一样。之前探索了Objective-C中的class和对象在C++中的原理实现知道对象存储是一个结构体,存储方式如:

struct Persion_2415_IMPL {                 //存储Persion的数据是一个struct Persion_2415_IMPL的数据结构
    struct NSObject_IMPL NSObject_IVARS;
    int _age_2415;
    NSString *_name_2415;
};

从上面存储的结构可以猜想,存储内存大小很可能和成员方法、类方法没有关系。测试之后发现,果然是没有关系的。

那么考虑我们oc的对象使用的内存,是否和变量的顺序有关系呢,进行如下测试

#import 
#import 

#import 

@interface Person : NSObject
@end
@implementation Person
{
    //isa
    double a; //[8-15]
    char b;   //[16]
    double c; //[24 - 31]
    char d;   //[32]
    double e; // [40 - 47]
    char f;   //[48]
    double g; // [56 - 63]
    char h;   // [64]
}

@end

@interface Student : NSObject
@end
@implementation Student
{
    //isa
    double a; //[8-15]
    double b; //[16-23]
    double c; //[24-31]
    double d; //[32-40]
    
    char e;   // [41]
    char f;   // [42]
    char g;   // [43]
    char h;   // [44]
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Person * per = [[Person alloc] init];
        Student * stu = [[Student alloc] init];

        NSLog(@"class_getInstanceSize -- %ld %ld",class_getInstanceSize([Person class]), class_getInstanceSize([Student class]));
        
        NSLog(@"malloc_size -- %ld %ld",malloc_size((__bridge void*)(per)), malloc_size((__bridge void*)(stu)));
        
    }
    return 0;
}

/*
输出结果:
2021-06-17 16:47:19.435118+0800 004-isa分析[8791:160576] class_getInstanceSize -- 72 48
2021-06-17 16:47:19.435456+0800 004-isa分析[8791:160576] malloc_size -- 80 48
*/

根据上面结果发现,原来真的会有内存使用的差别,那么我们可以根据这个方式来优化内存。再次探索,如果是作为属性的话,苹果内部会不会帮我们做一些优化呢,发现如果把上面的成员变量改成属性的话

/*
输出结果:
2021-06-17 16:29:54.056023+0800 004-isa分析[8613:153735] class_getInstanceSize -- 48 48
2021-06-17 16:29:54.056347+0800 004-isa分析[8613:153735] malloc_size -- 48 48
*/

由此可以知道,苹果帮我们使用属性的时候做了内存优化,所以我们如果要声明成员变量,一般最好使用属性的方式,能够更加节约内存。

2、为什么要进行内存对齐呢

由于CPU读取的时候很多是按照双字节,4字节读取的,如果不进行内存对齐的话,例如结构体

struct s1{
        char a;
        int b;
}
/*
    内存不对齐的情况下:
    char a 存储在 0 字节
    int b 存储在   1-4 字节
    需要读取b的时候,如果是4字节读取的,需要读2次,提取2次,组合一次
    0 - 3, 4 - 7, 然后再把 0-3中的1-3提取,和4-8中的4提取,再组合出 1-4
    
    如果内存对齐的情况下:
    char a 存储在 0 字节
    int b 存储在   4-7 字节
    需要读取b的时候,如果是4字节读取的,需要读1次,
    4 - 7 直接读取成功
    
    这是一种以空间换时间的方式,能够很好的提高读取的效率
*/

3、OC中的内存再探索

从刚刚的打印中我们有一个疑问,Person的class_getInstanceSize 和 malloc_size的大小并不一样,其中

  • class_getInstanceSize:对象至少需要的大小

  • malloc_size: 对象实质的大小

class_getInstanceSize的值,我们从结构体对齐中很容易得出原因。但是malloc_size为什么会不一样呢。我们开始探索OC中alloc时苹果的源码流程。之后再增加具体探索流程和代码阅读源码技巧

发现alloc的时候,会在class_getInstanceSize的基础上进行16字节的对齐。所以malloc_size大小会不一样。

总结:

  1. 我们可以在声明成员变量的时候,尽量的使用属性的方式,这时苹果会帮我们做好内存的优化。使得内存空间更加节省
  2. 对象的成员变量大小(包括isa)相加尽量是16的整数倍,可以减少一些浪费
  3. 对象的内存使用大小和对象方法,类方法都没有关系

你可能感兴趣的:(从结构体内存对齐到OC对象内存对齐)