阅读 329

OC对象原理探究(中)—— 结构体内存对齐

前言:很多iOS开发者会认为,”面试造航母,上班拧螺丝“,其实这存在一个误区!俗话说,万丈高楼平地起,地基是决定大楼稳定的重要因素,作为开发者也一样!一些底层知识的掌握,确实影响着一个人未来能在开发这条路能走多远,能够达到什么样的成就和高度!

既然认识到底层知识是如此重要,那我们就开始今天的主角探索 —— iOS开发中的内存对齐。

一、OC对象的本质

1、OC类的内部结构

OC对象原理探究(上)中,我们了解到,Objective-C代码是经过Clang中的LLVM编译器处理,先编译为C/C++代码,然后经过语法和词法分析和编译器优化,转化为汇编代码,最后生成可以执行的二进制代码。

  • 先从类的结构说起

NSObject是OC中所有类的总父类,于是我们可以创建一个YTPerson类,继承于NSObject类,如下所示:

//
//  main.m
//  OC对象的本质

#import <Foundation/Foundation.h>
#import <malloc/malloc.h>
#import <objc/runtime.h>

@interface YTPerson : NSObject
{
    NSString  *name;
    NSInteger age;
    double    weight;
}
@end

@implementation YTPerson

@end

int main(int argc, const char * argv[]) {
    
    YTPerson *person = [[YTPerson alloc] init];
    NSObject *objc = [[NSObject alloc] init];
    
    NSLog(@"\nYTPerson sizeof : %lu \nNSObject sizeof: %lu",
          sizeof(person),
          sizeof(objc));
    
    NSLog(@"\nYTPerson malloc_size: %lu \nNSObject malloc_size: %lu",
          malloc_size((__bridge const void *)person),
                      malloc_size((__bridge const void *)objc));
    
    NSLog(@"\nYTPerson class_getInstanceSize: %lu \nNSObject class_getInstanceSize: %lu",
          class_getInstanceSize([YTPerson class]),
          class_getInstanceSize([NSObject class]));
    
    return 0;
}


复制代码

2、 编译main.m文件到main.cpp

  • 编译指令(指定编译环境arm64下iphoneos)
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
复制代码

3、 查看main.cpp (关键代码)


/// runtime中定义了objc_class和Class的结构

// Class是一个结构体指针
typedef struct objc_class *Class;


/// objc_class的定义
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};


///编译的main.cpp中,NSObject和YTPerson的类结构

//NSObject的类结构
struct NSObject_IMPL {
	Class isa;              //isa 实际上是一个结构体指针,占用8字节
};

//YTPerson的类结构
struct YTPerson_IMPL {
	struct NSObject_IMPL NSObject_IVARS;   //相当于Class isa;  占用8字节
	NSString *_name;                       //字符串     占用8字节
	NSInteger _age;                        //NSInteger 占用4字节
	double _weight;                        //double    占用8字节
};

//main函数执行代码

int main(int argc, const char * argv[]) {

    YTPerson *person = ((YTPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((YTPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("YTPerson"), sel_registerName("alloc")), sel_registerName("init"));
    NSObject *objc = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));

    NSLog((NSString *)&__NSConstantStringImpl__var_folders_vk_kp1ndygs685bhx1m2rpsrnx40000gn_T_main_d20902_mi_0,
          sizeof(person),
          sizeof(objc));

    NSLog((NSString *)&__NSConstantStringImpl__var_folders_vk_kp1ndygs685bhx1m2rpsrnx40000gn_T_main_d20902_mi_1,
          malloc_size((__bridge const void *)person),
                      malloc_size((__bridge const void *)objc));

    NSLog((NSString *)&__NSConstantStringImpl__var_folders_vk_kp1ndygs685bhx1m2rpsrnx40000gn_T_main_d20902_mi_2,
          class_getInstanceSize(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("YTPerson"), sel_registerName("class"))),
          class_getInstanceSize(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("class"))));

    return 0;
}
复制代码
  • 函数输入打印结果:
函数输入打印结果:
2021-06-08 22:10:38.506568+0800 OC对象的本质[63464:15561542] 
YTPerson sizeof : 8 
NSObject sizeof: 8
2021-06-08 22:10:38.506924+0800 OC对象的本质[63464:15561542] 
YTPerson malloc_size: 32 
NSObject malloc_size: 16
2021-06-08 22:10:38.506979+0800 OC对象的本质[63464:15561542] 
YTPerson class_getInstanceSize: 32 
NSObject class_getInstanceSize: 8
复制代码
  • 3.1 sizeof、malloc_size和class_getInstanceSize的区别

分析:

① sizeof OC中Foundation框架里的函数,作用:返回一个变量或结构体占用多少字节数;

② malloc_size是C语言函数,作用:返回一个类实际占用的内存空间;

③ class_getInstanceSize是runtime函数,作用:返回一个类至少要占用多少存储空间。

4、 分析main.cpp

① 通过将OC代码,通过clang编译器编译成C++文件,可以看到YTPerson和NSObject对象的内部实现,实际上都是结构体类型。

② 由于YTPerson继承自NSObject对象,C++结构体的实现表现为:结构体的嵌套组合关系。

③ 从对象占用内存的大小上分析:

  • 经过计算:NSObject内的Ivars实际应该占用8字节,YTPerson内Ivars应该占用32字节;

  • 打印的数据,实际通过sizeof的对象内存:NSObject占用8字节,YTPerson占用8字节;

  • 打印的数据,实际通过malloc的对象内存:NSObject占用16字节,YTPerson占用32字节;

  • 打印的数据,通过runtime得到对象大小:NSObject大小是8字节,YTPerson大小为32字节。(本方式得到的大小与上面计算一致)

5、 猜想与假设

  • 对象内部实现实际上是结构体,取决于内部成员变量所占用的空间大小,也具有某种规律。猜想:大小可能是其中一个成员变量大小的n倍,n为正整数。

  • 那么这种猜想是否正确呢?让我们接下来进一步验证!

二、结构体的内存对齐

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的结果,必须是其内部最大成员的整数倍,不足的要对齐。

2、C/OC数据类型大小参照表

image.png

3、结构体内存对齐探究

  • 根据上面的猜想和结构体内存对齐的三个原则,我们可以确定分析YTPerson_IMPL的内存大小,如下所示:

//double 8  float 4 int 4 short 2   long 4  bool 1  char 1  NSString 8    NSInteger 4

struct YTPerson_IMPL {
	struct NSObject_IMPL NSObject_IVARS;   [0,1,2,3,4,5,6,7]
	NSString *_name;                       [8,9,10,11,12,13,14,15,16,17]
	NSInteger _age;                        18,19,[20,21,22,23]
	double _weight;                        [24,25,26,27,28,29,30,31]
};
//总结:YTPerson_IMPL内部最大成员大小包含结构体(占用8字节),总量32字节,满足结构体内存对齐原则三,结构体占用字节数为8x4 = 32字节,同时得出NSObject_IMPL大小实际为8字节。

复制代码

4、结构体内存对齐验证

  • 上面针对OC对象转C++结构体作了一系列分析,下面我们就专门针对结构体做一下内存分析,我们根据结构体对齐三大原则得出下面计算结果:

//结构体内存对齐探究
//double 8  float 4 int 4 short 2   long 4  bool 1  char 1  NSString 8

#import <Foundation/Foundation.h>

struct YTPeople {
    int         age;     //[0,1,2,3]
    double      weight;  //4,5,6,7[8,9,10,11,12,13,14,15] 
    float       height;  //[16,17,18,19]
    NSString    *name;   //20,21,22,23[24,25,26,27,28,29,30,31]
    char        *grade;  //[32]
} people;

//分析:内部成员占最大字节数:8字节,总量:33字节,要满足总量必须是最大成员整数倍,那么总量为8x5 = 40字节。


struct YTPerson {
    double      weight; //[0,1,2,3,4,5,6,7]
    NSString    *name;  //[8,9,10,11,12,13,14,15]
    char        *grade; //[16]
    int         age;    //17[18,19,20,21]
    float       height; //22,23[24,25,26,27]
} person;

//分析:内部成员最大字节数:8字节,总量28字节,要满足总量必须是最大成员整数倍,那么总量为8x4 = 32字节。


struct YTMan {
    long            numberOfFriends;//[0,1,2,3,4,5,6,7]
    BOOL            canFly;         //[8]
    int             age;            //9,10,11[12,13,14,15]
    struct YTPerson people;         //[16,17,18...45,46,47]
    
} man;

//分析:内部成员最大字节数:8字节,总量48字节,要满足总量必须是最大成员整数倍,那么总量为8x6 = 48字节。

int main(int argc, const char * argv[]) {
  
    NSLog(@"\n\n打印结构体大小:\npeople:%lu\nperson:%lu\nman:%lu\n\n",
    sizeof(people),
    sizeof(person),
    sizeof(man));
        
    return 0;
}

运行结果打印:
2021-06-08 23:32:02.251511+0800 结构体内存对齐探究[8103:15679304] 

打印结构体大小:
people:40
person:32
man:48

复制代码

三、结构体内存对齐过程

综上,根据结构体内存对齐三大原则来计算结构体的大小,确实和我们Xcode输出的sizeof计算结果一致,验证了结构体对齐三大原则的正确性。同时,也验证了我们的猜想的规律:结构体的大小是最大成员变量的大小的整数倍的结论。

我们接着猜想,如果系统按照我们上面的方式进行内存排序和查找,效率将会多低,所以,内存的排序方式肯定经过了一系列的优化,那么系统又是怎么对结构体的内存空间进行优化的呢?

🔆🔆🔆 在这里感谢您花费宝贵的时间来阅读本文,喜欢的话,点赞👍👍👍关注💋💋💋哦,我是浪迹天涯OL,为您持续创作有质量的文章!

文章分类
iOS
文章标签