OC底层原理(一):一个NSObject对象占多少内存?

2019-05-28  本文已影响0人  TheEnded

对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?
实际结果

image.png
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函数获得)

好了,第一节的内容就结束了,有不对的地方可以指出,欢迎大家讨论。

上一篇下一篇

猜你喜欢

热点阅读