美文网首页OC底层原理
OC底层原理(一):NSObject

OC底层原理(一):NSObject

作者: 跳跳跳跳跳跳跳 | 来源:发表于2020-12-09 21:27 被阅读0次

本系列用于记录学习过的底层知识。
我们从一道面试题开始入手

一个NSObject对象占用多少内存?

在回答这个问题之前,我们得弄懂NSObject到底是什么?
我们平时写的OC代码其底层都是C/C++来实现,所以我们将OC代码转换成C++来看看NSObject是怎么实现的。
首先,我们创建一个命令行项目,初始化一个NSObject对象

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

cd 到main.m文件路径下,使用

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

命令将OC代码转换成C/C++代码。


截屏2020-11-30 21.20.43.png

然后将生成的cpp文件拖入到项目中,不参与编译。


截屏2020-11-30 21.22.16.png 截屏2020-11-30 21.22.33.png

在cpp文件中,搜索NSObject_IMPL,可以看到以下结构体

struct NSObject_IMPL {
    Class isa;
};

这个结构体就是NSObject的底层实现。
isa又是什么呢?

typedef struct objc_class *Class;

isa就是一个指针,指向NSObject类对象的指针,类对象后面再详细说。
所以可以这么理解,前面初始化的NSObject对象底层就是一个带有isa指针的结构体。
在64位编译模式下,一个指针占8个字节,所以这个NSObject对象的大小为8个字节。我们可以通过class_getInstanceSize这个函数来印证下,导入头文件

#import <objc/runtime.h>

调用如下函数

//获取实例对象大小
NSLog(@"%zd", class_getInstanceSize([NSObject class]));

控制台打印如下输出


截屏2020-11-30 22.02.42.png

可以看出一个NSObject对象确实是占用8个字节,那么系统又会为NSObject对象分配多少内存呢?
我们可以通过malloc_size这个函数来查看,导入头文件

#import <malloc/malloc.h>

调用如下函数

//获取系统为person分配的内存大小
NSLog(@"%zd", malloc_size((__bridge const void *)(person)));

控制台打印如下输出


截屏2020-11-30 22.08.57.png

明明只需要8个字节,为什么又会分配16个字节呢?
因为系统会根据内存对齐规则来分配内存。


内存对齐

对齐规则:

1. 结构体内部第一个数据成员存在距离结构体本身地址偏移量为0的地址,其后的数据成员的偏移量为min(自身大小,对齐系数)的整数倍
2. 结构体的数据成员变量为结构体,那么此数据成员的偏移量为min(子结构体内部最大数据成员,对齐系数)的倍数
3. 最后结构体自身还要对齐一次,其大小为min(内部最大数据成员,对齐系数)的倍数

Xcode默认的对齐系数为8。

struct Struct1 {
    int a;//4  前面的偏移量为0,是min(自身大小,对齐系数)的倍数,不需要补齐,a的大小为4
    int b;//4  前面的偏移量为4,是min(自身大小,对齐系数)的倍数,不需要补齐,a和b的大小为8
    char c;//1 前面的偏移量为8,是min(自身大小,对齐系数)的倍数,不需要补齐,a、b、c的大小为9
    short d;//2 前面的偏移量为9,不是min(自身大小,对齐系数)的倍数,需要补齐1位变成10,a、b、c、d的大小为12
}myStruct1;//12为 min(最大数据成员大小(int b : 4),对齐系数)的倍数,不需要对齐

struct Struct1 {
    double a;//8  前面的偏移量为0,是min(自身大小,对齐系数)的倍数,不需要补齐,a的大小为8
    int b;//4  前面的偏移量为8,是min(自身大小,对齐系数)的倍数,不需要补齐,a和b的大小为12
    char c;//1 前面的偏移量为12,是min(自身大小,对齐系数)的倍数,不需要补齐,a、b、c的大小为13
    short d;//2 前面的偏移量为13,不是min(自身大小,对齐系数)的倍数,需要补齐1位变成14,a、b、c、d的大小为16
}myStruct1;//16为 min(最大数据成员大小(double a : 8),对齐系数)的倍数,不需要对齐

struct Struct2 {
    int a;//4  前面的偏移量为0,是min(自身大小,对齐系数)的倍数,不需要补齐,a的大小为4
    double b;//8  前面的偏移量为4,不是min(自身大小,对齐系数)的倍数,需要补齐4位变为8,a和b的大小为16
    char c;//1 前面的偏移量为16,是min(自身大小,对齐系数)的倍数,不需要补齐,a、b、c的大小为17
    short d;//2 前面的偏移量为17,不是min(自身大小,对齐系数)的倍数,需要补齐1位变成18,a、b、c、d的大小为20
}myStruct2;//最后myStruct2结构体自身对齐min(最大数据成员大小(double b : 8),对齐系数)的倍数,为24

接下来我们看另外一个类

@interface ZJPerson : NSObject
@property (nonatomic, assign) int age;
@property (nonatomic, assign) int height;
@end

这个类的底层实现类似于下面的结构体

struct ZJPerson_IMPL {
    Class isa;//8 前面的偏移量为0,是min(自身大小,对齐系数)的倍数,不需要补齐,isa的大小为8
    int _age;//4 前面的偏移量为8,是min(自身大小,对齐系数)的倍数,不需要补齐,isa、_age的大小为12
    int _height;//4 前面的偏移量为12,是min(自身大小,对齐系数)的倍数,不需要补齐,isa、_age、_height的大小为16
};//16为 min(最大数据成员大小(Class isa : 8),对齐系数)的倍数,不需要对齐

根据上面讲的内存对齐规则来分析,这个结构体大小为16

ZJPerson *person = [[ZJPerson alloc]init];
NSLog(@"instanceSize: %zd \n mallocSize: %zd", class_getInstanceSize([person class]), malloc_size((__bridge const void *)(person)));

通过上述方法来印证下


截屏2020-12-09 20.32.44.png

可以看到大小和上述分析的一致。
我们再看另外一个类

@interface ZJPerson : NSObject
@property (nonatomic, assign) int age;
@property (nonatomic, assign) int height;
@property (nonatomic, assign) int weight;
@end

我们再原来的基础上加了一个属性weight,我们再看看输出大小


截屏2020-12-09 20.35.22.png

为什么明明只需要24字节的结构体却分配了32个字节的内存空间呢?
这是因为系统在分配内存的时候还会再做一次内存对齐

#define NANO_MAX_SIZE           256 /* Buckets sized {16, 32, 48, ..., 256} */

不管是小内存还是大内存都是基于16的倍数来分配的,上面的结构体本来只需要24个字节,但是分配的时候第一档16字节不够,所以就分配了第二档32个字节。

总结

首先结构体内部成员会根据对齐规则1、2做一次对齐,得到一个大小,然后结构体自己会根据对齐规则3再做一次对齐,得到结构体的实际大小(instanceSize),最后系统在分配内存的时候还会做一次对齐操作,得到分配内存的大小(mallocSize)。

相关文章

网友评论

    本文标题:OC底层原理(一):NSObject

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