*Q1:一个NSObject实例对象占用多少内存?*
NSObject 对于每一个iOS开发者来说都很熟悉,因为我们几乎每时每刻都跟其打交道,但是我们可能不知道究竟这个熟悉的实例对象究竟占用我们内存的空间是多少呢?那么下面我们就一起来探讨一下。
NSObject *obj = [[NSObject alloc] init];
上面这段代码,相信大家很熟悉,就是alloc分配内存给对象,调用init方法为父类属性进行初始化,然后用指针obj指向我们刚才分配的地址。一个NSObject实例对象占用多少内存,其实就是在问这个obj指针指向的内存空间占用的内存空间是有多大?
要想解决这个问题:
①我们需要理解我们平常编写的OC代码其实就是基于C、C++为基础而"面向对象"的一门语言。
②其次需要知道的是NSObject在内存中究竟是如何布局的?
下面我们先来看看第一个问题解释:
我们日常编写的OC代码最终会在编译器的作用下转成C、C++语言、再到汇编语言、机器语言。通过下面的两张图可以发现OC代码编写的Student类和用C、C++代码写的strut结构体,有异曲同工之妙,那么我们是不是可以认为OC中的类底层代码其实就是C、C++的struct(结构体)?
确实是如此,那我们有没有方法去证明呢?答案是有的,我们可以通过新建一个Mac OS的命令行工程,在main函数中输入:
NSObject *obj = [[NSObject alloc] init];
然后打开我们的终端程序,输入
clang -rewrite-objc main.m -o main.cpp 或者
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
我们通过使用xcode自带的clang工具把OC代码转成C、C++代码,那上面的两个命令有什么不一样的?我们都知道OC这门语言不单单只能开发iOS上面的应用程序,而且能够开发MacOS、WatchOS等平台上的一些程序,而第一条命令会把OC代码转换成支持所有平台的C++程序,而第二条命令只会转换成arm64架构的c++程序。之所以编译成arm64架构支持的,是因为如今市面上的iPhone设备都使用该架构。
第一条命令和第二条命令生成的C++代码我们把其拉到最底部,能够发现我们main函数的代码,然后通过搜索NSObject_IMPL
找到 stuct NSObject_IMPL
结构体。如下图:
然后我们直接回到刚才编写的OC命令行程序,按住Command键点入NSObject类,我们能够看到我们NSObject其实就是下面这段代码:
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
再做一个简单的去除无用的代码即可得到:
@interface NSObject <NSObject> {
Class isa;
}
通过对比struct NSObject_IMPL
和 NSObject内部OC代码
,我们可以证明OC中的类底层代码其实就是C、C++的struct(结构体)。那回到我们一开始的问题:一个NSObject实例对象占用多少内存?我们可以发现struct NSObject_IMPL
中的isa
成员变量其实是typedef struct objc_class *Class;
这个种类型的指针。而我们知道一个类的地址是由他第一个成员变量的地址所决定的,也就是说如果isa
在内存中的地址为 0x100400110
那么这个NSObject实例对象
的地址值就是0x100400110
,因此我们能够得出结论指向一个NSObject
实例对象的指针obj
的地址就是0x100400110
我们又知道一个指针在64位系统中占用的内存空间就是8个字节,那么我们可能会觉得只有一个isa
指针的NSObject实例对象
,它所占用的内存空间就是8个字节,其实这是不对的,其实一个NSObject实例对象
占用的内存空间为16个字节,为什么呢?我们不妨通过两个函数来验证一下。通过使用<objc/runtime.h>
中的class_getInstanceSize
方法和<malloc/malloc.h>
中的malloc_size
方法来打印一下NSObject实例对象
所占用的内存空间。
NSLog(@"class_getInstanceSize: %zd",class_getInstanceSize([NSObject class]));
NSLog(@"malloc_size: %zd",malloc_size((__bridge const void *)(obj)));
打印出来的分别是8和16,那为什么我们刚才说的是16个字节而不是8个字节呢?而且这两个方法有什么区别呢?为什么是以malloc_size为标准呢?其实<objc/runtime.h>
中的class_getInstanceSize
方法所得到的是objc对象实际需要的内存大小,而<malloc/malloc.h>
中的malloc_size
方法所得到的是objc对象实际分配的内存大小。那为什么一个NSObject对象明明只需要8个字节的内存大小就可以了,但是还是分配到了16个字节大小的内存空间?对于这个问题我们可以通过阅读objc4源码来得到答案,地址https://opensource.apple.com/tarballs/ 。下载最新版本的objc4源码。
通过跟踪obj4中alloc和allocWithZone两个函数的实现,会发现这个连个函数都会调用一个instanceSize的函数:
size_t instanceSize(size_t extraBytes) {
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16bytes.
if (size < 16) size = 16;
return size;
}
这个函数的代码很简单,返回的结果就是系统给一个对象分配内存的大小。当对象的实际大小小于16时,系统就返回16个字节的大小。也就是说16个字节大小是系统的最低消费。还是用坐车的例子来说明一下,假如有8个人想坐车,他们打电话叫车说要一辆能坐8个人大小的车,对方说sorry我们没有坐8个人大小的车,我们这里最小的就是坐16个人的车。最后来了一辆坐16个人的车,拉了8个人开走了。车就好比一个NSOject对象,车上的乘客就好比是对象中的成员,车的大小或者说载客数量就相当于一个对象占用的内存大小,车上实际的乘客数量就是对象中成员的大小。所以说一个NSObject对象占用多少内存,我想应该很明白了。
总结:
系统分配了16个字节给NSObject 对象(通过malloc_size获得)
但NSObject对象内部只使用了8个字节(64bit 通过class_getInstanceSize)
另外:
我们可以通过view memory、lldb的指令去验证我们的结论,这些操作会再后面制作的视频讲解中附上。
下一节:
会继续深入,推算针对我们自定义的类内存布局和对象占用的内存空间。