一,Objective-C的本质

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

所以Objective-C的面向对象,都是通过C/C++的数据结构实现的。

Objective-C的对象,是用C++中的结构体来实现的。

二,猜测

接下来我们通过代码来验证下OC对象的本质。OC代码如下:

1
2
3
4
5
6
7
8
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *objc = [[NSObject alloc] init];
        NSLog(@"Hello, World!");
    }
    return 0;
}

我们使用命令行工具将一下代码转化为C++代码,命令如下:

1
clang -rewrite-objc main.m -o main.cpp

我们可以指定架构,命令如下:

1
2
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 
生成 main-arm64.cpp 

接下来,我们在main-arm64.cpp文件中,探索NSObject对象的本质,可以找到NSObject_IMPL(即NSObject对象转化为C++后的实现)

1
2
3
4
5
6
struct NSObject_IMPL {
    Class isa;
};
// 查看Class本质
typedef struct objc_class *Class;
我们发现Class其实就是一个指针,对象底层实现其实就是这个样子。

通过以上代码,发现NSObject对象转换为结构体后,结构体成员中只有一个isa指针,指针在arm64架构下,大小为8个字节,在32位下是4个字节,也就是说一个NSObject对象在64位所占用的内存空间为8个字节,32位下是4个字节。

三,验证

1,代码层次:

我们通过class_getInstanceSize()来打印NSObject对象的大小,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#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;
    }
}

打印结果如下:

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

我们在runtime源码中查看class_getInstanceSize的具体实现,看看获取的到底是什么占用的内存。

1
2
3
4
5
size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}

继续点进去alignedInstanceSize如下:

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

ivar是成员变量的意思,通过注释我们大概可以猜到,此方法是获取对象的成员变量占用内存的大小。

2,Xcode lldb工具验证

我们在代码中打个断点,如下:

断点之后,可以看到:

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

这个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`同样也能得出

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

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

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

更复杂的对象

Student对象

定义如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@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;
    }
}

由以上得出的结论,我们猜测,Student对象,中含有一个继承NSObject来的isa指针,占用8个字节,_no和_age各占用4个字节,总共占用8 + 4 + 4 = 16个字节。

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

1
2
3
4
5
struct Student_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _no;
    int _age;
};

而NSObject_IMPL结构体,我们在上边的分析已经知道,其中只有一个isa指针,所以Student_IMPL结构体可以写为:

1
2
3
4
5
struct Student_IMPL {
    Class isa;
    int _no;
    int _age;
};

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

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

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

可以很清晰的看到红框的八个字节存放的是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所占的存储空间:

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

输出结果:

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

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

对拥有Person父类的Student对象的分析
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@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对象的分析,我们可以构建下图:

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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字节是空出来没有被利用的,下图是其内存结构,灰色部分是空闲的。

那么对于Student_IMPL的_no成员变量来说,它的存储位置是接在灰色区域之后,把灰色区域继续空出来还是把灰色区域利用起来呢?答案是把灰色区域利用起来。Student_IMPL的内存结构如下图:

所以一个Student实例对象所占的内存空间也是16字节。

1
2
3
4
5
6
7
8
        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]));

打印结果:

1
2
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属性。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@interface Person:NSobject
{
    
    @public
    int _no;
}
@property (nonatomic, assign) int height;

@end

@implementation Person
@end

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

1
2
3
4
5
struct Person_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _age;
    int _height;
};

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