OC对象的本质<一>

面试问题:

  • 一个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 implemention
struct NSObject_IMPL {
    Class isa;
};

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

@interface NSObject  {
    Class isa  OBJC_ISA_AVAILABILITY;
}

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

#import 

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 
#import 
#import 

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自带的工具来验证我们刚才的结论

我们在代码中打个断点:


OC对象的本质<一>_第1张图片
4E40A483-E979-46FC-AE8D-24DA063AB8CA.png

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


OC对象的本质<一>_第2张图片
9CC80E85-7DBB-4A50-995A-625863DA1A4A.png

这样我们就可以获得objc对象的地址为:0x604000005ff0。
然后我们在xcode菜单栏中找到Debug->Debug Workflow->View Memory,在address中输入0x604000005ff0,回车就得到:
OC对象的本质<一>_第3张图片
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同样也能得出:
    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检查一下:
    36C8CCE5-7688-4F8C-BADC-4433AF5B98A7.png

    指定内存中的值确实修改了。
  • p,po
    p是print的简写,它可以用来打印非对象类型的数据,比如读取int,bool类型的值。
    po是print object的简写,它是用来打印对象的,比如我们使用po object看看得到什么:
    505E2E1B-2590-4E58-B27A-3ED3BC8C789D.png
Student对象

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

@interface Student:NSObject
{
    @public
    int _no;
    int _age;
}
@end

@implementation Student
@end

int 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的工具查看内存:

OC对象的本质<一>_第4张图片
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实例对象的内存结构大概就是下图这样:


OC对象的本质<一>_第5张图片
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对象的分析,我们可以构建下图:


OC对象的本质<一>_第6张图片
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字节是空出来没有被利用的,下图是其内存结构,灰色部分是空闲的。
    OC对象的本质<一>_第7张图片
    0D160963-EA4A-474E-A991-04CD9F165104.png

    那么对于Student_IMPL的_no成员变量来说,它的存储位置是接在灰色区域之后,把灰色区域继续空出来还是把灰色区域利用起来呢?答案是把灰色区域利用起来。Student_IMPL的内存结构如下图:
    OC对象的本质<一>_第8张图片
    EAC31B8D-F806-4F09-A242-BB9A0B5FD008.png

    所以一个Student实例对象所占的内存空间也是16字节。
        Student *student = [[Student alloc] init];
    
        Person *person = [[Person alloc] init];
        
        //获得student实例对象的成员变量所占的大小
        NSLog(@"student实例对象的成员变量所占的存储空间:%zd", class_getInstanceSize([Student class]));        
        //获得person实例对象的成员变量所占的大小
        NSLog(@"person实例对象的成员变量所占的存储空间:%zd", class_getInstanceSize([Person class]));

打印结果:

2018-06-26 19:33:52.467400+0800 interview1-OC对象的本质Student[12656:386270] student实例对象的成员变量所占的存储空间:16
2018-06-26 19:33:52.468997+0800 interview1-OC对象的本质Student[12656:386270] person实例对象的成员变量所占的存储空间:16

打印结果也就验证了我们的推测。

属性和方法

我们给Person类增加一个height属性。

@interface Person:NSobject
{
    
    @public
    int _no;
}
@property (nonatomic, assign) int height;

@end

@implementation Person
@end

那么Person_IMPL结构体会变成什么样子呢?转化后找到Person_IMPL:

struct Person_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _age;
    int _height;
};

我们可以看到增加了_height成员变量,这和我们所学的OC知识:声明一个属性的同时也就声明了一个成员变量是一致的。
我们创建出来的实例对象中只有成员变量,为什么没有存放方法呢?
每个实例对象中都有一份成员变量,因为每个实例对象都可以有自己的成员变量值,每个实例对象的成员变量值都可以不一样,所以需要在每个实例对象中存放所有的成员变量。但是方法就不一样了,每个对象执行的方法都是一样的,只需要保存一份就够了,没有必要在每个实例对象中都保留一份方法。

你可能感兴趣的:(OC对象的本质<一>)