OC 对象本质

  1. 一个 NSObject 对象占用多少内存?
  2. 对象的 isa 指针指向哪里?
  3. 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++ 的代码
    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;
    }
}

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这个结构体的大小。

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;
};

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


5796542-af4d6b8740f948a2.png

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

这里涉及到一个概念:大端模式和小端模式。

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

Mac OS系统使用的是大端模式。所以较高的有效字节存储在较低的存储器地址,所以04 00 00 00的正确值就是00 00 00 04即4。

对拥有Person父类的Student对象的分析

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

首先我们不把代码转化为C++的源码,根据前面对NSObject对象和Student对象的分析,我们可以构建下图:
5796542-b5c13572a0f4a486.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字节是空出来没有被利用的,下图是其内存结构,灰色部分是空闲的。
5796542-537fcc1d162a8a70.png

那么对于Student_IMPL的_no成员变量来说,它的存储位置是接在灰色区域之后,把灰色区域继续空出来还是把灰色区域利用起来呢?答案是把灰色区域利用起来。Student_IMPL的内存结构如下图:
5796542-6bda1fe87cd88e77.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 对象本质)