本系列用于记录学习过的底层知识。
我们从一道面试题开始入手
一个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++代码。

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


在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]));
控制台打印如下输出

可以看出一个NSObject对象确实是占用8个字节,那么系统又会为NSObject对象分配多少内存呢?
我们可以通过malloc_size这个函数来查看,导入头文件
#import <malloc/malloc.h>
调用如下函数
//获取系统为person分配的内存大小
NSLog(@"%zd", malloc_size((__bridge const void *)(person)));
控制台打印如下输出

明明只需要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)));
通过上述方法来印证下

可以看到大小和上述分析的一致。
我们再看另外一个类
@interface ZJPerson : NSObject
@property (nonatomic, assign) int age;
@property (nonatomic, assign) int height;
@property (nonatomic, assign) int weight;
@end
我们再原来的基础上加了一个属性weight,我们再看看输出大小

为什么明明只需要24字节的结构体却分配了32个字节的内存空间呢?
这是因为系统在分配内存的时候还会再做一次内存对齐
#define NANO_MAX_SIZE 256 /* Buckets sized {16, 32, 48, ..., 256} */
不管是小内存还是大内存都是基于16的倍数来分配的,上面的结构体本来只需要24个字节,但是分配的时候第一档16字节不够,所以就分配了第二档32个字节。
网友评论