我们来思考一下,当我们写下下面这一行代码,以在现在主流的arm 64架构中为例,它会占用多少的内存空间?
NSObject *obj = [[NSObject alloc] init];
以上,obj是指向这个NSObject对象的指针,我们想知道这个NSObject对象占用了多少内存,首先先来了解一下NSObject在底层中是如何实现的。
NSObject的底层实现
我们都知道:我们平时编写的Objective-C代码,底层实现其实都是c/c++代码,所以其实Objective-C的面向对象都是基于c/c++的数据结构实现的。
那么:Objective-C的对象、类主要是基于c/c++的什么数据结构实现的?
稍微学过一点Objective-C的应该都能脱口而出:结构体。
我们来验证一下,将包含如下代码的main.m文件编译为c/c++的代码,看看它的实现:
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
}
return NSApplicationMain(argc, argv);
}
终端中cd到main.m文件所在的上一层目录,然后在终端中输入:
clang -rewrite-objc main.m -o main.cpp
然后回车,在main.m文件同目录下,我们就得到了main.cpp这样的c++文件。打开main.cpp文件之后发现代码多达10万行,我们可以通过指定编译的具体平台、架构来简化一下它,如指定在iOS平台(xcrun -sdk iphoneos)下的arm64架构(-arch arm64):
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
在得到的main.app文件中,我们找到定义NSObject的地方:
struct NSObject_IMPL {
Class isa;
};
这就是NSObject的底层实现,它就是一个结构体,这个结构体里只有一个类型为Class的成员变量isa。
那么Class是什么呢?点进去看它的内部,它其实是一个指针:
typedef struct objc_class *Class;
NSObject对象的内存大小
我们知道在arm64架构中一个指针的大小是8个字节,而NSObject_IMPL这个结构体就isa这一个成员,结构体的大小根据其成员变量来决定的, 按理来说arm64架构下,一个NSObject对象的大小就应该是8个字节,实际是不是这样的呢?
我们通过runtime库中的一个可以获取某个类的实例对象的内存大小的API:class_getInstanceSize(Class _Nullable cls),来打印一下:
NSLog(@"%zd", class_getInstanceSize([NSObject class]));
输出:8
既然输出8,那是不是就证实了我们上面的猜想,arm64架构下,一个NSObject对象的大小就是8个字节?
别着急下结论,我们再来看一下:
malloc库中有提供可以获取指针所占内存大小的API:malloc_size(const void *ptr),它接收一个类型为const void *的指针的参数
NSLog(@"zd", malloc_size((__bridge const void *)obj));
输出:16
那么到这里是不是就疑惑了,到底哪个是对的?
既然有疑惑,我们就尝试去苹果的源码中找答案,objc4源码地址:opensource.apple.com/tarballs/ob…
本次分析源码版本为:objc4-818.2
我们先来看一下class_getInstanceSize(Class _Nullable cls)的实现:
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
这里它调用了cls里的方法alignedInstanceSize(),从函数名我们就可以看出这是进行内存对齐原则操作的函数,我们继续追进alignedInstanceSize():
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
alignedInstanceSize()返回的是类的成员变量所占据的大小。 所以,我们应该说,一个NSObject对象创建的时候是分配了16个字节给它,只不过它真正利用起来的只有8个字节。
我们来进一步论证一下:
我们知道,alloc方法就是在通过调用allocWithZone:方法给对象分配内存,那么我们来看allocWithZone:的内部实现:
// Replaced by ObjectAlloc
+ (id)allocWithZone:(struct _NSZone *)zone {
return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}
跟进:
NEVER_INLINE
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
// allocWithZone under __OBJC2__ ignores the zone parameter
return _class_createInstanceFromZone(cls, 0, nil,
OBJECT_CONSTRUCT_CALL_BADALLOC);
}
按照一般情况下的走法,此时会调用class_createInstanceFromZone,我们继续跟进去看它的实现:
static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
int construct_flags = OBJECT_CONSTRUCT_NONE,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
ASSERT(cls->isRealized());
// Read class's info bits all at once for performance
bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
size_t size;
size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (zone) {
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);
}
if (slowpath(!obj)) {
if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
return _objc_callBadAllocHandler(cls);
}
return nil;
}
if (!zone && fast) {
obj->initInstanceIsa(cls, hasCxxDtor);
} else {
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}
if (fastpath(!hasCxxCtor)) {
return obj;
}
construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
return object_cxxConstructFromClass(obj, cls, construct_flags);
}
来到这里,一般情况下也是会走到obj = (id)calloc(1, size);,通过调用calloc这个c语言底层的分配内存的函数来分配内存,那么这里的传参size值得我们注意。
我们往前几行来看变量size:
size = cls->instanceSize(extraBytes);
这里先是通过调用alignedInstanceSize()函数 + 前面传进来的extraBytes给size赋值,extraBytes前面传的是0,而alignedInstanceSize()就是上面我们跟进class_getInstanceSize()时跟到的,上面的class_getInstanceSize()打印为8。
inline size_t instanceSize(size_t extraBytes) const {
if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
return cache.fastInstanceSize(extraBytes);
}
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
我们的重点在下一行:
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
当当当当!CoreFoundation 要求所有对象至少为 16 个字节。
至此,我们可以看出,一个NSObject对象分配内存时在来到size = cls->alignedInstanceSize() + extraBytes这里时,等于:
size = cls->alignedInstanceSize() + extraBytes; = size = 8 + 0; = 8;
当执行完if (size < 16) size = 16,它所要分配到的内存就是16个字节了。