为了账号安全,请及时绑定邮箱和手机立即绑定

OC对象的本质<一>

标签:
Cocos2d-x

面试问题:

  • 一个NSObject对象占用多少内存?

  • 对象的isa指针指向哪里?

  • OC的类信息存放在哪里?

int main(int argc, char * argv[]) {    @autoreleasepool {        NSObject *objc = [[NSObject alloc] init];        return 0;
    }
}

第一个问题实质上就可以转化为objc这个指针指向的内存区域有多大。为了搞清这个问题,我们就要搞清楚NSObject在内存中是怎么布局的,它的底层原理。

Objective-c的本质

我们平时编写的objective-c代码,底层实现其实都是C/C++代码

Objective-C > C/C++ > 汇编语言 > 机器语言

所以Objective-C的面向对象都是基于C/C++的数据结构实现的。
Objective-C的对象,类主要是基于C/C++的结构体来实现的。

  • 将Objective-c代码转化为C/C++的代码:
    1>在命令行cd到放Objective-c代码的文件夹
    2>比如我们要把文件夹中的main.m文件转化,我们可以再命令行输入:clang -rewrite-objc main.m,然后在这个文件夹下我们就得到了转化成功的文件main.cpp。
    我们将上面的代码转化为C++的源码,得到main.cpp。
    在7000多行我们找到这样一个结构体:

//NSObject implementionstruct NSObject_IMPL {
    Class isa;
};

这个结构体就是NSObject对象在内存中的本质。
另外,我们按住command点击进NSObject里面看一下,也可以看到这样一个结构:

@interface NSObject <NSObject> {    Class isa  OBJC_ISA_AVAILABILITY;
}

这和C++源码中的结构体极为相似,也证实了NSObject对象的本质就是一个C++结构体。
我们把NSObject_IMPL这个结构体复制到main.m文件中:

#import <Foundation/Foundation.h>struct NSObject_IMPL {
    Class isa;//在64位中占8字节,32位中占4字节。};int main(int argc, char * argv[]) {    @autoreleasepool {        NSObject *objc = [[NSObject alloc] init];        return 0;
    }
}

然后我们按住command键点击Class进入窥探一下这个Class到底是个什么东东,我们看到这样一个结构:

typedef struct objc_class *Class;

这说明Class是一个结构体指针。所以isa也就是一个指针。因此NSObject_IMPL这个结构体中就是包含了一个结构体指针isa,它所占的内存大小就是这个isa指针所占的内存大小。
在64位环境中,指针占8个字节,在32位环境中,指针占4个字节。
NSObject_IMPL这个结构体只有一个成员isa指针,所以结构体的地址就是存放isa指针的地址。比如isa这个指针的地址是0x100400100,那么就有objc=0x100400110。

所以一个NSObject对象在64位环境中占8字节,在32位环境中占4字节。我们接着往下看,通过读取内存来验证我们的想法。

  • class_getInstanceSize()方法
    class_getInstanceSize()返回的NSObject_IMPL的大小。

#import <Foundation/Foundation.h>#import <objc/runtime.h>#import <malloc/malloc.h>struct NSObject_IMPL {
    Class isa;
};int main(int argc, char * argv[]) {    @autoreleasepool {        
        NSObject *objc = [[NSObject alloc] init];        
        //获得NSObject类的NSObject_IMPL结构体的大小
        NSLog(@"class: %zd", class_getInstanceSize([NSObject class]));        return 0;
    }
}

打印结果:

2018-06-25 21:09:04.070852+0800 interview1-OC对象的本质[16368:450669] class: 8

我们查看一下class_getInstanceSize的具体实现,看看它获取的到底是什么占用的内存,我们从runtime的源码中可以找到class_getInstanceSize的实现:

size_t class_getInstanceSize(Class cls){    if (!cls) return 0;    return cls->alignedInstanceSize();
}

然后我们继续点进这个alignedInstanceSize()里面看看:

// Class's ivar size rounded up to a pointer-size boundary.
    uint32_t alignedInstanceSize() {        return word_align(unalignedInstanceSize());
    }

ivar是成员变量的意思,通过注释我们大概知道这个函数获取的是结构体的成员变量所占的内存的大小,也即是NSObject_IMPL这个结构体的大小。
下面我们回答一下第一个面试题:

  • 一个NSObject对象占用多少内存?
    在32位系统中占4字节,在64位系统中占8字节。

我们还可以通过xcode自带的工具来验证我们刚才的结论

我们在代码中打个断点:


webp

4E40A483-E979-46FC-AE8D-24DA063AB8CA.png


然后我们在下面可以看到:


webp

9CC80E85-7DBB-4A50-995A-625863DA1A4A.png


这样我们就可以获得objc对象的地址为:0x604000005ff0。
然后我们在xcode菜单栏中找到Debug->Debug Workflow->View Memory,在address中输入0x604000005ff0,回车就得到:

webp

2E376D19-FEF4-44F2-8187-282FD9C856E2.png


这个xcode工具的作用就是查看从输入的这个地址开始,后面的内存地址的情况。我们可以看到第一排中A8,7E,3B,01,00,00,00,它们是十六进制,所以一个数字表示4位,那么两个数字组合在一起就是一个字节。所以A8 7E 3B 01 00 00 00就是8个字节,按照之前得出的结论,这8个字节中存放的是isa指针。

如果我们不喜欢这种图形化工具,还可以使用LLDB指令。
  • memory read
    例如刚才窥探从0x604000005ff0开始的内存,我们也可以用LLDB指令进行:
    memory read 0x604000005ff0同样也能得出:

    webp

    5B5ABF87-B7FD-4C5B-A504-0C36C5D4F3F4.png


    memory write还可以简写为x,即memory read 0x604000005ff0等同于x 0x604000005ff0

  • memory write
    有memory read就有memory write,如果我们想改变内存中指定内存地址的值,可以使用memory write。比如,我们使用的地址是0x604000005ff0,那么我们想改变从这个基地址开始的第9个字节内的值,我们可以这样写:
    memory write 0x604000005ff8 8,然后我们x 0x604000005ff0检查一下:

    webp

    36C8CCE5-7688-4F8C-BADC-4433AF5B98A7.png


    指定内存中的值确实修改了。

  • p,po
    p是print的简写,它可以用来打印非对象类型的数据,比如读取int,bool类型的值。
    po是print object的简写,它是用来打印对象的,比如我们使用po object看看得到什么:

    webp

    505E2E1B-2590-4E58-B27A-3ED3BC8C789D.png

Student对象

下面我们来看一下一个更复杂的OC对象-Student对象。Student对象有两个成员变量_no和_age。
那么一个Student类的实例对象占有多少内存呢?大家心里可能都有了自己的答案。

@interface Student:NSObject{    @public
    int _no;    int _age;
}@end@implementation Student@endint main(int argc, char * argv[]) {    @autoreleasepool {
        Student *student = [[Student alloc] init];        return 0;
    }
}

同样,我们还是把main.m文件转化为C++的源码。我们在main.cpp中通过command+f搜索Student_IMPL这个东西,我们为什么要搜索这个东西呢?因为我们在学习NSObject对象时找到了NSObject_IMPL这个结构体,果然,我们也找到了Student_IMPL这个结构体:

struct Student_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _no;    int _age;
};

NSObject_IMPL其实我们已经很熟悉了,我们还是点进去看看:

struct NSObject_IMPL {
    Class isa;
};

所以Student_IMPL这个结构体的第一个成员就是一个NSObject_IMPL结构体,第二个第三个成员分别是Student类的成员变量。由于NSObject_IMPL这个结构体就占8字节,它里面的成员isa也是占8个字节,那么Student_IMPL结构体就可以改写成下面这样:

struct Student_IMPL {
    Class isa;    int _no;    int _age;
};

所以我们知道一个Student的实例对象在内存中占8+4+4=16个字节空间。并且三块内存空间是连续的。假设isa的地址是0x100400110,那么_no的地址就是0x100400118,_age就是0x10040011C。那么我们怎样验证我们的结论呢?首先使用指针给成员变量赋值:

student->_no = 4;
student->_age= 5;

然后我们在程序中打个断点查看student指针的地址为0x600000014d10。再利用xcode的工具查看内存:

webp

87449745-BD1F-490D-B74F-44F4108FA385.png


可以很清晰的看到红框的八个字节存放的是isa指针,绿框的四个字节存放的是_no成员变量,黄框的四个字节存放的是_age成员变量。并且我们可以看到绿框中四个字节存放的内容是04 00 00 00,这和_no成员变量的值好像很吻合,又好像有一点不对,同样,_age成员变量也是这样。这是为什么呢?
这里涉及到一个概念:大端模式和小端模式。


大端模式:较高的有效字节存放在较低的存储器地址,较低的有效字节存放在较高的存储器地址。
小端模式:较高的有效字节存放在较高的的存储器地址,较低的有效字节存放在较低的存储器地址。

Mac OS系统使用的是大端模式。所以较高的有效字节存储在较低的存储器地址,所以04 00 00 00的正确值就是00 00 00 04即4。
下面我们再用另外一种方式来证明我们的结论,我们使用在NSObject对象中使用过的class_getInstanceSize()读取Student_IMPL所占的存储空间:

//获得student实例对象的成员变量所占的大小
 NSLog(@"student实例对象的成员变量所占的存储空间:%zd", class_getInstanceSize([Student class]));

输出结果:

2018-06-26 18:33:36.642604+0800 interview1-OC对象的本质Student[11339:336714] student实例对象所占的存储空间:16

输出结果再次证明了我们刚才的结论!
student实例对象的内存结构大概就是下图这样:


webp

0A4E26FC-B041-45AD-9922-C49A8996DA13.png

对拥有Person父类的Student对象的分析
@interface Person:NSObject{    int _age;
}@end@implementation Person@end@interface Student:Person{ 
    @public
    int _no;
}@end@implementation Student@end

Student类继承自Person类,Person类又继承自NSObject类,Person类有一个成员变量_age,Student类有一个成员变量_no。那么问题来了,Student实例对象和Person实例对象在内存中各占多少存储空间呢?
首先我们不把代码转化为C++的源码,根据前面对NSObject对象和Student对象的分析,我们可以构建下图:


webp

15030C3C-6CB5-44D7-B6DA-A2B8ED40EE8A.png


下面我们把main.m转化为C++的源码验证一下

struct NSObject_IMPL {
    Class isa;
};struct Person_IMPL {
    struct NSObject_IMPL NSObject_IVARS;//8个字节
    int _age;     //4个字节};struct Student_IMPL {
    struct Person_IMPL Person_IVARS;
    int _no;
};

这和我们预期的是完全一样的。
首先我们来分析一下Person实例对象占多少存储空间:
我们知道一个NSObject_IMPL结构体占8字节,一个int型的成员变量占4字节,那么是不是一个Person实例对象就占12字节的空间呢?实际上不是的。原因有二:

  • 1.一个OC对象至少占有16字节的存储空间,低于16字节是肯定不对的。

  • 2.有一个原则叫内存对齐简而言之就是一个结构体的空间大小一定是其占有内存空间最大的成员变量的内存的整数倍。Person_IMPL结构体占内存最大的成员变量是struct NSObject_IMPL NSObject_IVARS,所以Person对象所占内存应该是8的倍数,结合还有一个成员变量的大小是4字节,所以Person对象所占内存空间大小就是16字节。
    我们再来分析Student对象:
    Student_IMPL有两个成员变量,其中Person_IVARS这个成员变量,我们已经分析过了,占16字节,而_no这个成员变量占4字节,然后再结合内存对齐原则,Student_IMPL结构体就是占32字节,事实上是不是这样呢?其实这样分析是有问题的。
    问题就出在,Person_IMPL这个结构体占用的16个字节其实没有全部利用,而是为了满足内存对齐原则等。其实在这16字节的最后4字节是空出来没有被利用的,下图是其内存结构,灰色部分是空闲的。

    webp



作者:雪山飞狐_91ae
链接:https://www.jianshu.com/p/395b5f53ca63


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消