对OC底层原理探究的开篇,也是对mj课程的回顾总结,尽量能都记录下来吧。
这里其实也是一道面试题,那么我们就看看这个答案是什么。
问:一个NSObject对象占多少内存?
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
}
return 0;
}
我们都知道OC编译之后会成为C/C++代码,那么编译之后到底是什么样子呢?XCode的默认编译器是clang,我能可以通过clang命令直接编译并运行一段OC代码。
打开terminal,进入mian.m文件所在的路径,
输入以下指令把OC代码转成C++(编译之后是C/C++,使用cpp后缀对C来说也正常)
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
可以看到当前文件夹下多了一个main-arm64.cpp文件(图中main.cpp文件是未指定架构变异出来的文件)
image.png
为了方便查看我们把这个文件拖入工程中。
在文件中搜索NSObject_IMPL,可以找到NSObject的编译之后的样子
image.png
其实我们也知道直接按住command进入NSObject也可以看到,它是这个样子
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
对于Class我们按住command再进去,它是这个样子
typedef struct objc_class *Class;
其实就是NSObject结构体中有一个Class结构体类型的指针,注意是指针,指针在64位系统中占8个字节。
--------分割线--------
那么它到底是不是占8个字节呢,我们可以log一下。
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
NSLog(@"%zd", class_getInstanceSize([NSObject class]));
NSLog(@"%zd", malloc_size((__bridge const void *)obj));
}
return 0;
}
最终结果
image.png
我们知道class_getInstanceSize方法是获取实例变量的大小,malloc_size是分配内存大小。对于class_getInstanceSize方法获取的大小应该是我们能想到的,但是malloc_size为什么会返回16呢?我们继续看源码,我们首先看class_getInstanceSize,这个方法在runtime源码中可以查到,我这里直接贴出来
//两个方法在不同的地方这里一起放过来了
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
// Class's ivar size rounded up to a pointer-size boundary.
//我们看到这里注释就是说返回ivar的大小(注意这里的一些修饰词rounded up 、 boundary 以及方法word_align)
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}
所以这里返回8就是正确的了。我们再看malloc_size,这个是指的分配内存,分配内存我们知道是调用allocWithZone:的时候进行分配,我们再找下这个的具体实现。
image.png
进入这个方法,最后我们找到_class_createInstanceFromZone这个方法 image.png
在看instanceSize的实现,我们就能知道这里为什么是分配了16个字节大小的空间。
image.png
到这里就结束了吗?继续看。
如果我们有一个Person类,Person类里有一个成员变量int _age,分析下下面的代码
@interface Person : NSObject
{
int _age;
}
@end
@implementation Person
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc]init];
NSLog(@"person - %zd", class_getInstanceSize([Person class]));
NSLog(@"person - %zd", malloc_size((__bridge const void *)person));
}
return 0;
}
这里会输出什么?我们知道int占据4个字节大小,根据我们上面所看到的,Person最终转换为这个样子
struct NSObject_IMPL {
Class isa;
};
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS; // 8
int _age; // 4
};
所以我们猜应该是12 ,16?看结果吧
image.png
竟然都是16 ,为什么呢?
这就就是继续看上面那个alignedInstanceSize方法,我让大家注意这里的关键词,其实这里是有一个计算机内存对齐的原因,其中有1条是,结构体的大小是结构体内部成员大小最大的那个的整数倍(这里是不准确的说法,其中还涉及到操作系统的#pragram pack()指定系数,具体就百度吧)。这里内部成员大小最大的就是isa指针了,长度是8,所以要是8的整数倍,12的基础上补上4位也就是16。
你以为到这里就完了?我们再继续,我们在person中再添加一个int _no; int _height;
这次的输出结果是什么呢?
根据上面讲的,Person编译后应该是这样
struct NSObject_IMPL {
Class isa;
};
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS; // 8
int _age; // 4
int _no; //4
int _height; //4
};
关于内存对齐我们也了解,所以推断出这里log应该是24(8+4+4+4+补齐4), 24?
实际结果
what F***?为什么实力对象占用24字节,分配内存却给了32呢?我们明明是只看到了不足16返回16呀。
这里其实又涉及到了内存分配的对齐,大家注意和结构体的内存对齐是不一样的。这里内存分配其实是按照一个bucket,iOS堆空间的内存分配时,这里是16。也就是分配的内存是16的倍数。
具体可以看源码。打开网址https://opensource.apple.com/tarballs/,搜索libmalloc,选择编号最大的一份下载(编号最大=最新)。
PS:这个分配内存的源码太复杂了,其实我也没有找到在哪写的对齐是16...,有了解的同学可以给指点一下。
面试题解答
问:一个NSObject对象占用多少内存?
答:NSObject对象只占用8个字节(64bit下)
系统分配了16个字节给NSObject对象(通过malloc_size函数获得)
好了,第一节的内容就结束了,有不对的地方可以指出,欢迎大家讨论。
网友评论