问题提出:
解析过程
要知道NSObject对象占用多少内存,需要知道创建一个NSObject对象的时候都做了什么。
比如下面这行代码:
NSObject *object = [[NSObject alloc] init];
首先我们点进去看看NSObject是怎么定义的
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
然后发现NSObject的声明中只有一个class 类型的isa指针成员变量,按住command键看一下class又是个什么东西呢。点进去我们发现,class是一个指向结构体的指针,具体是什么这个结构体是什么我们先不管。
typedef struct objc_class *Class;
另外我们知道平时编写的Objective-C代码底层实现其实是C/C++代码,然后转成汇编语言,最后生成机器阅读的机器语言也就是01010110这种。
也就是:
既然这样,那我们就看看NSObject 转成C/C++代码是什么样的。
通过下面任何一种方式都可以将OC语言转成C/C++
方式一:clang -rewrite-objc main.m(main.m是要被转C/C++的文件名字) -o main.cpp(main.cpp是要生成C/C++的文件名字)
方式二:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m(这里是要被转C/C++的文件名字) -o main-arm64.cpp(main-arm64.cpp是要生成C/C++的文件名字)
方式一是将OC直接转成C/C++ 语言,(注:-o main.cpp 这个是要生成C/C++的文件名,可写可不写)
方式二是将OC转成适应于 64位真机下的 C/C++ 语言,-o main-arm64.cpp这行可写可不写,默认生成<原类名.cpp>文件
在此我们也举一个简单的例子,直接创建一个macOS项目就好(等下后面运行不需启动模拟器)。
然后在main.m文件的main函数里简单写一行代码
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obejct = [[NSObject alloc] init];
}
return 0;
}
接着打开终端。cd到文件目录下,然后选择上面任何一种转成C/C++的方式,这里我用方式二
即:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
然后就会生成一个 main-arm64.cpp 文件,我们打开看一下。
直接搜索NSObject,就会发现有这么一个结构体
struct NSObject_IMPL {
Class isa;
};
此处我们也可以得之,OC的类在C/C++中实现的方式就是一个结构体,即OC类是以结构体的形式在内存中存在的。而且NSObject内部只有一个class 类型的isa指针,既然是一个指针,那么我们知道,指针在64环境下就是占8个字节(在32位环境下就是4个字节。)既然这个指针占用8个字节,而且NSObject这个结构体内部只有这么一个结构体,很显然,这个结构体也就是占用8个字节。
所以,
NSObject *obejct = [[NSObject alloc] init];
这句代码的过程就是:
[NSObject alloc]在内存中分配地址,然后创建对象。接着将object这个指针指向刚刚创建的对象,最后将分配的对象地址赋值给object指针。也就是
那么这样看来,创建一个NSObject对象具体分配的大小 就是8个字节,因为前面咱们已经分析出,NSObject底层实现方式其实就是一个结构体,而这个结构体内部只有一个占8个字节的isa指针。到底是不是呢需要验证一下。
runtime里面有这么一个函数,获取类
的实例大小
class_getInstanceSize(Class _Nullable cls)
既然有这么个方法,那咱们就打印验证一下,看是不是8个字节呢,
注意要先引入
#import <objc/runtime.h>
NSLog(@"NSObject InstanceSize:%zd",class_getInstanceSize([NSObject class]));
结果:NSObject InstanceSize:8
经过测试发现,“果然”是8个字节。有眼尖的小伙伴可能会发现这里可能会有坑。为了确保安全,我们需要看看 class_getInstanceSize(Class _Nullable cls)
这里是OC源码地址
https://opensource.apple.com/tarballs/
打开链接往下翻或直接搜索 objc4
,点进去会发现好多版本,然后找到最大的那个,也就是最新的下载、解压。打开解压好的源码,这是不能直接运行的,但是我们可以大略看一下。
打开之后直接搜索 class_getInstanceSize
,找.mm文件看实现代码
我们发现,在调用class_getInstanceSize(Class _Nullable cls)
函数的时候,如果传入的类对象为nil,就会直接返回0。否则就调用alignedInstanceSize
函数,也就是对齐后的实例大小
,接着点击去发现
调这个方法的意思是:返回unalignedInstanceSize(未对齐的实例大小)对齐后的大小
这也是遵循了内存分配法则之一:内存对齐原则。但这个函数的注释是返回class的成员变量大小,注意,此处可有有蹊跷了。既然这个函数式返回对象的成员变量的大小,那么这个大小是不是就是创建对象时分配的大小呢?为了证明我们也看看
NSObject
的alloc
方法是怎么实现的。对象调用alloc
方法其实就是调用了allocwithzone
,我们直接在刚刚下载的OC源码里搜索allocwithzone
,找到这个函数的实现,发现在这个函数里调用了一个创建实例对象的函数接着我们看看这个函数式怎么实现的,点进去发现这里面有调用了一个获取实例对象大小的函数
然后我们再进一步点进去发现里面计算的size是调用了instanceSize(size_t)
这个函数
我们接着点进去,最后找到这个,也就是实例对象的大小
我们发现这个函数的有一行注释,意思就是CoreFoundation框架规定所有的对象至少要16个字节。也就是如果这个对象不足16个字节,也会分配给你16个字节。
虽然这么说,我们也得验证一下
alloc
在创建的时候是不是真的分配了16个字节。首先我们在之前自己创建的文件里导入
#import <malloc/malloc.h>
然后会查看这个函数
extern size_t malloc_size(const void *ptr);
/* Returns size of given ptr */
malloc的全称是memory allocation,中文叫动态内存分配,用于申请一块连续的指定大小的内存块区域以void*类型返回分配的内存区域地址。extern size_t malloc_size(const void *ptr) 函数的意思就是返回一个指针的占用内存的大小,既然这样我们就把创建的object传进去,看看大小到底是多少,
NSLog(@"obejct: %zd",malloc_size((__bridge const void *)(obejct)));
结果:obejct malloc_size: 16
打印结果的确是16个字节。
根据打印结果结合源码解析我们就可以得知创建NSObject
对象的时候系统会分配给其16个字节,但是这个对象只使用了8个字节,另外8个字节是空着的。
而且咱们也可以根据对象地址查看内存空间地址,首先在创建NSObject的时候添加个断点,打印一下obejct
对象的地址:0x10062f930,然后找Debug->Debug Workflow -> View Memory,接着将地址输入,就可以查看。
根据内存空间地址,我们也可以发现。isa占用了前面8个字节(因为查看的是16进制,一位16进制代表4位二进制,所以两位16进制就是8位二进制也就是一个字节),而后面八个字节都是00,再后面就是其他内存了。
简单画张图表也就是页面这个样子
虽然弄明白了NSObject,但是开发最多的还是用自定义的类。下面就看一下自定义的类所占的内存空间大小。
在此为了方便查看,我就直接在main文件直接创建一个SSPerson类,类里面有两个成员变量
@interface SSPerson : NSObject{
@public
int _age;
int _height;
}
@end
@implementation SSPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
}
return 0;
}
然后我们再次main.m文件转为C/C++文件,看看自定义的SSPerson类转成C/C++是什么格式的,方法和前面一样。
然后搜索SSPerson就会发现,
struct SSPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
int _height;
};
我们发现自定的SSPerson类也是转成了一个结构体,内部包含三个成员变量,下面两个是咱们自己写的,NSObject_IVARS 是一个struct NSObject_IMPL结构体类型,前面我们说过,NSObject
底层实现就是struct NSObject_IMPL,而其内部就只有一个class
类型的 isa 成员,所以这里可以直接当做是Class isa,
即
struct SSPerson_IMPL {
Class isa;
int _age;
int _height;
};
那么根据之前分析(后面如果没有特别说明都是结合64位环境下计算),isa占8个字节,而且int类型占用4个字节,这样算来,就是
一个isa 8个字节 + int
类型 4个字节 + int
类型 4个字节 = 16个字节。
我们也可以进一步查看SSPerson对象的内存空间地
第一个是isa占用8个字节,接着是_age占用4个字节,然后是_height占用4个字节。
那么SSPerson是不是占16个字节,我们打印一下便知。
NSLog(@"SSPerson InstanceSize:%zd",class_getInstanceSize([SSPerson class]));
NSLog(@"person malloc_size: %zd",malloc_size((__bridge const void *)(person)));
结果为:
SSPerson InstanceSize:16
person malloc_size: 16
由此可我们也可以得知,创建SSPerson对象的时候系统会分配给其16个字节,而且其使用了16个字节。
如果感兴趣,可以进一步尝试,多尝试就会有多发现。
答案:
*系统分配了16个字节给NSObject对象(通过malloc_size函数获得)
*但NSObject对象内部只使用了8个字节的空间(64bit环境下,可以通过class_getInstanceSize函数获得)