【iOS】OC底层系列一 - 对象的本质

面试题:一个NSObject对象占用多少内存?

一、OC对象的本质

1.1. OC的本质

我们平时编写的Objective-C代码,底层实现其实都是C\C++代码。所以Objective-C的面向对象都是基于C\C++的数据结构实现的。

思考:Objective-C的对象、类主要是基于C\C++的什么数据结构实现的?
答案:结构体,因为只有结构体才能容纳不同类型的数据。

将Objective-C代码转换为C\C++代码:

1
$ xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 输出的CPP文件

如果需要链接其他框架,使用-framework参数,比如-framework UIKit

1.2. NSObject

思考:一个OC对象在内存中是如何布局的?

创建一个NSObject对象:

1
2
3
4
5
// main.m文件
int main(int argc, const char * argv[]) {
NSObject *object = [[NSObject alloc] init];
return 0;
}

NSObject在API中的定义如下,只有一个成员变量:

1
2
3
4
5
@interface NSObject {
Class isa;
}
// Class是一个结构体指针
typedef struct objc_class *Class;

main.m文件的代码转换为C++代码后发现,NSObject被转换成了结构体实现:

1
2
3
struct NSObject_IMPL {
Class isa;
}

指针在64bit环境下占用8个字节,是不是意味着创建的object对象占用8个字节呢?我们通过打印观察一下。

1
2
3
4
5
6
7
8
9
// 获得NSObject实例对象的成员变量所占用的大小(至少需要多少内存)
// 需要导入runtime库:#import <objc/runtime.h>
// 输出:8
NSLog(@"%zd", class_getInstanceSize([NSObject class]));

// 获得object指针所指向内存的大小(实际分配多少内存)
// 需要导入malloc库:#import <malloc/malloc.h>
// 输出:16
NSLog(@"%zd", malloc_size((__bridge const void *)object)

创建NSObject对象时,分配内存16个字节,实际占用8个字节。

object指针保存的是NSObject对象内存地址,而对象的内存地址其实就是指针isa的内存地址(因为只有一个成员变量),所以object指针指向的是isa的内存地址。

为什么分配了16个字节,而仅占用8个字节?这个和objc的源代码有关,苹果官方定义:在创建对象时,分配的内存至少16个字节alignedInstanceSize对应的objc源码是class_getInstanceSize的实现。

NSObject对象内存布局方式:

Xcode查看内存布局:

读取内存:

1.3. 自定义对象

创建一个Student类(继承NSObject)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@interface Student : NSObject
{
@public
int _no;
int _age;
}
@end

@implementation Student

@end

int main(int argc, const char * argv[]) {
Student *stu = [[Student alloc] init];
stu->_no = 3;
stu->_age = 10;
return 0;
}

思考:Student对象占用多少内存?

1
2
3
4
5
// 输出:16
NSLog(@"%zd", class_getInstanceSize([Student class]));

// 输出:16
NSLog(@"%zd", malloc_size((__bridge const void *)stu));

内存分配和实际占用都是16个字节。因为Student继承自NSObject,所以也会有一个isa指针,而Student类有两个int类型的成员变量(int在64bit环境占用4个字节),所以一共占用isa + _no + _age = 16个字节。

把Student对应的类转为C++后也可以看到,Student类把NSObject的成员变量也继承过去了。

1
2
3
4
5
6
7
8
9
struct NSObject_IMPL {
Class isa;
}

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

Student对象内存布局方式:

读取内存:

1.4. 继承

创建一个Person类(继承NSObject),Student类(继承Person)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@interface Person : NSObject
{
int _age;
}
@end

@implementation Person

@end

@interface Student : Person
{
int _no;
}
@end

@implementation Student

@end

思考:一个Person对象、一个Student对象占用多少内存空间?

经过1.2和1.3的内容,基本上就可以知道Person对下和Student对象都是占用16个字节内存。

把对应代码转为C++代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct NSObject_IMPL {
Class isa;
}

struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
}

struct Student_IMPL {
struct Person_IMPL Person_IVARS;
int _no;
}

Person类使用class_getInstanceSize获取到类的成员变量占用16个字节。为什么不是12?一是因为内存对齐的缘故:结构体的大小必须是最大成员变量大小的倍数。由于Person类占用内存的最大成员变量是isa,所以Person对象占用16个字节。二是操作系统分配内存的原因:在iOS系统中,堆空间分配的内存大小是16的倍数

class_getInstanceSize对应的objc源码是alignedInstanceSize:返回的是内存对齐后的大小。

Student对象为什么不是占用20个字节(Person结构体占用16个字节 + Student的成员变量_no占用4个字节)?因为Person对象的16个字节中,最后的4个字节是空着的,而Student继承自Person,所以会把Person对象分配的最后4个字节使用_no占用。

1.5. 属性和方法

创建一个Person类(继承NSObject)。

1
2
3
4
5
6
7
8
9
10
@interface Person : NSObject
{
int _age;
}
@property (nonatomic, assign) int height;
@end

@implementation Person

@end

思考:增加一个属性后,Person对象占用多少内存?

其实属性最终也会生成一个带下划线的成员变量,因此Person对象占用16个字节。

把对应代码转为C++代码:

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

疑问:属性不是会自动生成setter和getter方法么?为什么还是占用16个字节?

因为创建对象时(alloc)不会把方法放到对象的内存中,方法在内存中只有一份就够了。比如,创建很多个Person对象,每一个对象都有自己的ageheight等成员变量,但是对应的setter和getter方法对于每个对象都是一样的,因此不会把方法放到对象内存中(放在方法列表中,具体介绍在后续章节会体现)。

苹果开源代码:https://opensource.apple.com/tarballs/。

  1. objc相关的代码在objc4文件夹下,进入文件夹就可以选择下载最新版本代码(数字越大,版本越新)。

  2. alloc相关的代码在libmalloc文件夹下。


面试题:一个NSObject对象占用多少内存?
解答:系统分配了16个字节给NSObject对象(通过malloc_size函数获得),但NSObject对象内部只使用了8个字节的空间(64bit环境下,可以通过class_getInstanceSize函数获得)。