美文网首页iOS
iOS开发之结构体底层探索

iOS开发之结构体底层探索

作者: 俊而不逊 | 来源:发表于2021-06-10 09:59 被阅读0次

说在前面

我们平时写的代码Objective-C,底层实现其实都是C/C++的代码实现的,高级语言经过编译器编译,最终转化为机器语言。


编译器编译流程.png

所以,我们的Objective-C的面向对象,其实都是基于C/C++的数据结构实现的。那么Objective-C的对象、类主要是基于C/C++的什么数据结构实现的呢?

1.对象的本质

那到底是什么样的数据结构结构?是数组吗?我们都知道数组只能存储同一种类型的数据,而对象会有不同的属性,比如Student这个类,它有姓名(string),身高(double),等等都是不同的数据类型,很显然不是数组的结构类型!那么很显然只有一种结构能满足,那就是结构体(struct)。那到底是不是呢?我们来探索一下。

我们建立一个工程,然后编译成C++看看

编译前


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

@autoreleasepool {

NSObject *obj = [[NSObject alloc]init];

}

return 0;

}

输入一下面这个命令进行编译

clang -rewrite-objc main.m -o main.cpp

我们看到输出了一个C++的文件main.cpp

打开编译后的文件可以看到,main函数变成了底下这个屌样子


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

/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;

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

}

return 0;

}

不同平台下面的代码是不一样,比如Windows,macOS,iOS,那么我们肯定是希望是支持iOS系统下的C++代码,那么用下面这个命令

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc 源文件 -o 输出文件

意思就是Xcode编译是跑在arm64架构的iPhone平台上的,-o是输出的意思

运行如下命令

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp

编译完的底层源码有好几万行,在7400行,我们可以看到,NSObject的底层是结构体

NSObject底层结构

struct NSObject_IMPL {

Class isa;

};

这也就验证了OC的底层是结构体,那么这个Class isa是个什么东东呢?

我们进入里面看看


typedef struct objc_class *Class;

这个 *Class就是指向结构体的指针,那么指针在结构里面占多少字节呢?如果是64位就是8个字节,32位就是4个字节,那么现在的系统都是64位的了,在这个结构体里面,Class isa成员变量就是占8个字节。那么这个结构体也就是8个字节,因为现在这个结构体里面没有其他的属性和变量,这个Class isa成员变量是默认带上的,所以结构体就是占8个字节。

2.结构体

下面的这个代码是实例化出了一对象obj,其实底层实现就是上面👆讲的一个结构体,里面会有一个 Class isa成员变量


NSObject *obj = [[NSObject alloc]init];

假如isa地址是0x12300001obj这个指针的地址是多少呢???

alloc已经分配了内存,那么这个指针就是首地址,而里面只有一个成员变量,那么isa的地址就是结构体在内存中的地址,那么obj=0x12300001

结构体的大小是指针的大小,那NSObject对象在内存中的大小是多少呢?是不是也是8个字节呢???其实不是的,是16字节。啊???为什么是16字节呢???一脸疑惑脸🤔,那我们接着往下探索👇

3.内存对齐

我们可以打印看看NSObject实例对象的成员变量所占用的内存大小

引入头文件

#import<objc/runtime.h>


#import<objc/runtime.h>

NSLog(@"InstanceSize:%zd",class_getInstanceSize([NSObject class]));

我们可以看到输出的结果是8那我们再看看,obj所指向的内存的大小

导入头文件#import<malloc/malloc.h>


NSLog(@"malloc_size:%zd",malloc_size((__bridge const void *)(obj)));

打印结果如下

从打印的结果来看,是16,这也就验证了上面👆说的:NSObject对象在内存中的大小是16字节。po打印一下

也可以查看地址在内存中的分布

Debug->Debug Workflow->View Memory

View Memory

下图就是内存分配情况

View Memory内存分布

从上图很明显看出来是16字节,前8位是isa,后8位就是内存分配预留的8字节。那为什么要预留呢?明明8个就够用了,分配16干嘛???内存资源是很珍贵的啊!CPU是不是傻啊???带着这个疑问,我们继续往下探索👇

下面的代码打印结果是多少呢???


@interface Student : NSObject

{

int _age;

int _num;

}

@end

@implementation Student

@end

Student *stu = [[Student alloc]init];

NSLog(@"InstanceSize:%zd",class_getInstanceSize([Student class]));

NSLog(@"malloc_size:%zd",malloc_size((__bridge const void *)(stu)));

NSLog(@"sizeof:%lu",sizeof(stu));

打印结果


通过命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp转为cpp文件是如下结构


struct Student_IMPL {

Class isa;

int _age;

int _num;

};

因为Student是继承自NSObject,Student的结构体里面有个成员变量isa是指向父类NSObject的。isa8个字节,int类型是4个字节,一共就是16字节。那我Student里面少一个成员变量呢?那结果是12还是16呢????

打印结果

结果是16,why???为什么呢?这就是字节对齐class_getInstanceSize是计算类的成员变量的大小,实际上计算的并不是严格意义上的对象的内存的大小,因为内存进行了8字节对齐,从objc的底层源码可以看到,核心算法是define WORD_MASK 7UL ((x + WORD_MASK) & ~WORD_MASK

各种基本数据类型在内存中所占字节的大小

补充:sizeof不是一个函数,是C/C++中的一个操作符(operator)sizeof()是一个判断数据类型或者表达式长度的运算符。

实际上对象的内存对齐是16字节对齐,我们继续往下探索👇

4.内存对⻬的原则

  1. 数据成员对⻬规则:结构(struct)(或联合(union))的数据成员,第

⼀个数据成员放在offset为0的地⽅,以后每个数据成员存储的起始位置要

从该成员⼤⼩或者成员的⼦成员⼤⼩(只要该成员有⼦成员,⽐如说是数组,

结构体等)的整数倍开始(⽐如int字节,则要从4的整数倍地址开始存

储。 min(当前开始的位置m 大小n ) 比如: m = 9 n = 4 --> 9 10 11 12

  1. 结构体作为成员:如果⼀个结构⾥有某些结构体成员,则结构体成员要从

其内部最⼤元素⼤⼩的整数倍地址开始存储(struct a⾥存有struct b,b

⾥有char,int,double等元素,那b应该从8的整数倍开始存储.)

  1. 收尾⼯作:结构体的总⼤⼩,也就是sizeof的结果,必须是其内部最⼤

成员的整数倍不⾜的要补⻬。

5.结构体内存对齐

桂花上代码,先上几个结构体尝尝味儿,哈哈😁


struct Student1{

double a;      // 8    [0 7]

char b;        // 1    [8]

int c;          // 4    (9 10 11 [12 13 14 15]

short d;        // 2    [16 17] 24

} Student1;

struct Student2{

double a;      // 8    [0 7]

int b;          // 4    [8 9 10 11]

char c;        // 1    [12]

short d;        // 2    (13 [14 15] 16

} Student2;

NSLog(@"Student1:%lu-Student2:%lu",sizeof(Student1),sizeof(Student2));

打印结果

下面就开始细细的品尝这两道开胃小菜吧!根据内存对齐原则进行简单的计算和分析

Student1内存大小详细过程min(m,n) ,m表示当前开始的位置,n表示大小)

  1. a: 占8个字节,offert从0开始, min(0,8), 即0 ~ 7 存放a

  2. b: 占1个字节,offert从8开始, min(8,1), 即8 ~ 8 存放b

  3. c: 占4个字节,offert从12开始,min(12,4),即12 ~ 15 存放c,中间9、10、11不是4的倍数,所以得空出来。

  4. d: 占2个字节,offert从14开始,min(16,2),即16~17 存放d

下面👇放上Student1的内存分布图,便于理解

Student1内存分布

根据对齐原则3,结构体的总⼤⼩,必须是其内部最⼤

成员的整数倍不⾜的要补⻬Student1中最大的是8,所以最后为24

Student2内存大小分析

  1. a: 占8个字节,offert从0开始, min(0,8), 即0 ~ 7 存放a

  2. b: 占4个字节,offert从8开始, min(8,4), 即8 ~ 11 存放b

  3. c: 占1个字节,offert从12开始,min(12,1),即12 ~ 12 存放c

  4. d: 占2个字节,offert从14开始,min(14,2),即14~15 存放d

下面👇放上Student2的内存分布图,便于理解

Student2内存分布情况.png

为什么d的存放不从13开始,因为13不是2整数倍,所以从14开始,根据对齐原则,最后为16

开胃菜吃完了,那就再来道硬菜,7788。

桂花上菜!!!!!


struct Student3 {

double a; // 8 [0 7]

int b;    // 4    [8 9 10 11]

char c;  // 1    [12]

short d;  // 2    (13 [14 15]

int e;    // 4    [16 17 18 19]

struct Student1 str;//(20 21 22 23 [24 ~ 47]

}Student3;

NSLog(@"Student1:%lu-Student2:%lu-Student3:%lu",sizeof(Student1),sizeof(Student2),sizeof(Student3));

打印结果如下

打印结果

Student3内存大小分析

  1. a: 占8个字节,offert从0开始, min(0,8), 即0 ~ 7 存放a

  2. b: 占4个字节,offert从8开始, min(8,4), 即8 ~ 11 存放b

  3. c: 占1个字节,offert从12开始,min(12,1),即12 ~ 12 存放c

  4. d: 占2个字节,offert从14开始,min(14,2),即14~15 存放d

  5. e: 占4个字节,offert从16开始,min(16,4),即16~19 存放e

这道Student3菜确实有点硬啊!得好好啃一啃了。Student3的其他成员就不做过多分析了,上面也有,主要分析下这个Student3里面的struct Student1 str,这是一个嵌套的结构体,结构体里面还嵌套了一个结构体。成员struct Student1 str其实就是Student1结构体,上面我已经知道了Student1的内存大小是24了,而Student3里面的其他成员所占用的是19Student1里面成员的最大值是8,所以offert必须是8的倍数,也就是从24开始,连续开辟24字节的内存空间来存储struct Student1 str

为了便于理解,画了下面👇这张图

Student3内存大小分析

6.总结

  1. 对象的本质是结构体,可以使用命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc 源文件 -o 输出文件查看底层结构,对象的内存对齐是16字节对齐

  2. 获取类的成员变量的大小用:class_getInstanceSize查看

  3. 获取一个对象实际开辟的内存大小用:malloc_size查看

  4. 结构(struct)(或联合(union))的数据成员,第⼀个数据成员放在offset为0的地⽅,以后每个数据成员存储的起始位置要从该成员⼤⼩或者成员的⼦成员⼤⼩(只要该成员有⼦成员,⽐如说是数组,结构体等)的整数倍开始(⽐如int字节,则要从4的整数倍地址开始存

储。

  1. 如果⼀个结构⾥有某些结构体成员 ,则结构体成员要从其内部最⼤元素⼤⼩的整数倍地址开始存储,也就是按8字节对齐,因为指针的大小就8

  2. 结构体的总⼤⼩,必须是其内部最⼤成员的整数倍不⾜的要补⻬

🌹请收藏+关注,评论 + 转发,以免你下次找不到我,哈哈😁🌹

🌹欢迎大家留言交流,批评指正,互相学习😁,提升自我🌹

相关文章

网友评论

    本文标题:iOS开发之结构体底层探索

    本文链接:https://www.haomeiwen.com/subject/qprueltx.html