学习MJ的视频课程,整理总结知识点–OC对象的本质

[TOC]

Objective-C是一种通用、高级、面向对象的编程语言。它扩展了标准的ANSI C编程语言,将Smalltalk式的消息传递机制加入到ANSI C中。当前主要支持的编译器有GCC和Clang(采用LLVM作为后端)维基:Objective-C

Objective-C和C_C++

想探究OC对象的本质,我们可以逐步深入。我们知道,最了解一个编程语言的,莫过于它的编译器了,而我们知道Xcode内置的编译器的前端是clang,clang是一个C、C++、Object-C的轻量级编译器。Objective-C兼容C语言,我们可以通过clang编译器将Objective-C代码重写成更基础的C/C++代码,这样就能看到Object-C在C/C++下的等价实现。但是使用Xcode构建ObjC项目时并不会先将Objective-C代码翻译为C++,这是因为clang编译器支持直接编译Objective-C代码。

(本文为了书写方便,将Object-C简写为OC)

Objective-C对象数据结构

我们知道OC是基于C语言扩展出来的,C是没有对象的概念,那么OC的对象是基于什么结构实现的呢?

或许我们曾经通过Xcode看过objc.h的头文件,发现如下代码

1
2
3
4
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};

同时我们知道,结构体里面可以存放不同类型的数据。我们或许猜测到NSObject的底层结构是基于结构体。

事实上OC对象确实是基于结构体的实现,下面我们通过重写源码进行验证。

我们以简单main.m函数代码为例,新建一个如下图所示的工程,然后通过clang命令把main函数及内部的NSObject重写为C++的实现,即可了解objc对象的实现。

1
2
3
4
5
6
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
}
return 0;
}

终端切换到main.m所在的目录,然后执行clang命令,如下:
(关于命令的使用我们可以通过clang --help来了解)

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

打开main.cpp文件,如下图,我们在最后看到main.m函数的实现,同时还可以一窥OC的消息机制

1
2
3
4
5
6
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
NSObject *obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
}
return 0;
}

同时,我们在main.cpp可以看到这样的一段代码

1
2
3
4
// NSObject_IMPL (NSObject_IMPLEMENTATION)
struct NSObject_IMPL {
Class isa;
};

即说明了NSObject对象是通过结构体实现的,且内部只有一个Class(结构体)指针

NSObject的内存本质

通过上面的内容,我们已经知道NSObject对象的底层实现是结构体,且内部只有一个isa结构体指针,这个指针指向Class的起始地址。
NSObject的底层实现

class_getInstanceSize、malloc_size

知道了NSObject的内存结构后,接下来我们探讨一个NSObject对象占用多少内存?

思路:通过上面的小节,我们已经知道NSObject对象内部只有一个isa结构体指针,我们知道在64位中操作系统中,指针占用内存空间是8个字节,所以我们推测一个NSObject对象应该只占8个字节,事实是否如此?我们可以利用系统提供的malloc_size函数进行验证。

通过系统提供的函数即可获得,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];

// 获得NSObject实例对象的成员变量所占用的大小 >> 8
NSLog(@"%zd", class_getInstanceSize([NSObject class]));

// 获得obj指针所指向内存的大小 >> 16
NSLog(@"%zd", malloc_size((__bridge const void *)obj));

}
return 0;
}

更进一步,我们可以研究class_getInstanceSize、malloc_size这两个函数的实现,因为苹果已经开源了objc,我们可以查找相应的实现代码从而做到知其所以然。

苹果的源码开放地址为:Source Browser,如果想研究苹果的源码一般都是访问这个地址。我们下载objc4的最新源码,然后解压打开源码。搜索我们要找的源码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() const {
return word_align(unalignedInstanceSize());
}

从而得出结论:class_getInstanceSize是获得NSObject实例对象的成员变量所占用的空间大小(isa指针所占的空间),占用的空间大小8字节。

但是我们通过malloc_size获取到的空间大小是16字节,为什么?

接下来我们探究malloc_size的实现。我们知道malloc_size是调用的alloc方法,而alloc会调用allocWithZone,我们在源码中搜索allocWithZone,会在NSObject.mm文件中看到类似如下的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
// Call [cls alloc] or [cls allocWithZone:nil], with appropriate 
// shortcutting optimizations.
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
if (slowpath(checkNil && !cls)) return nil;
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
return _objc_rootAllocWithZone(cls, nil);
}
#endif
// ...
}

我们研究_objc_rootAllocWithZone的实现会发现如下调用顺序:
_objc_rootAllocWithZone ->_class_createInstanceFromZone

我们搜索_class_createInstanceFromZone的实现,会有如下发现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
int construct_flags = OBJECT_CONSTRUCT_NONE,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
ASSERT(cls->isRealized());

// Read class's info bits all at once for performance
bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
size_t size;

size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;

id obj;
if (zone) {
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);
}
...

然后发现关键的地方,变量size的值是通过instanceSize函数返回的,我们查找instanceSize的源码,然后会有如下发现

1
2
3
4
5
6
7
8
9
10
size_t instanceSize(size_t extraBytes) const {
if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
return cache.fastInstanceSize(extraBytes);
}

size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}

上面的源码有两个关键的地方

  1. alignedInstanceSize
    我们在上面已经知道它返回的是成员变量所占用的空间大小,NSObject对象占用的大小为8字节。

  2. // CF requires all objects be at least 16 bytes.
    我们可以知道分配的size一旦小于16,CoreFoundation框架会返回最小值16。

至于alignedInstanceSize函数以及CF框架为什么会是16,这里不再深究,感兴趣的可以搜索内存对齐。这里简述一下:内存对齐(数据结构对齐)是方便访问,OC对象占用的空间大小都是16的倍数,结构体的内存对齐最小单元是8字节

这里做一下总结:
系统分配了16个字节给NSObject对象(通过malloc_size函数获得),但是NSObject内部只占用了8个字节的空间(64为环境下,可以通过class_getInstanceSize函数获得)

实时查看内存数据

View Memory

了解一个对象所占用内存具体情况,我们可以利用Xcode的菜单栏上的Debug -> Debug Workfllow -> View Memory,来查看一个对象的内存占用情况。
屏幕快照 2020-04-26 上午10.08.44

备注:内存中0000…一般代表未使用的空白空间

LLDB

我们还可以通过Xcode提供的LLDB进行断点调试

1
2
print、p:打印地址
po:打印对象

读取内存 memory read (简写为x)
例:x/4xw 0x100550c20

1
2
memory read/数量格式字节数 内存地址
x/数量格式字节数 内存地址

修改内存中的值 memory write
例: memory write 0x100550c29 10

1
memory  write  内存地址  数值

自定义对象(Student)的本质

我们研究NSObject对象后,继续研究简单的自定义对象,以Student对象为例,我们可以根据NSObject的内存结构推断出下面的结果。我们同样可以使用clang对这里的OC源码重新进行验证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
struct Student_IMPL {
Class isa;
int _no;
int _age;
};

@interface Student : NSObject {
@public
int _no; // 4个字节
int _age; // 4个字节
}
@end

@implementation Student
@end

int main(int argc, const char * argv[]) {
@autoreleasepool {
Student *stu = [[Student alloc] init];
// 通过指针直接访问成员变量
stu->_no = 4;
stu->_age = 5;

NSLog(@"%zd", class_getInstanceSize([Student class])); // 16个字节 (8 + 4 + 4)
NSLog(@"%zd", malloc_size((__bridge const void *)stu)); // 16个字节

// 等价结构体
struct Student_IMPL *stuImpl = (__bridge struct Student_IMPL *)stu;
NSLog(@"no is %d, age is %d", stuImpl->_no, stuImpl->_age); // no is 4, age is 5
}
return 0;
}

Student的内存布局

Student对象占用了16个字节的内存空间,前8个字节放isa指针,接下来4位放_no变量,最后4位放_age变量。
Student内存布局

我们还可以通过View Memory验证,以及通过LLDB重写内存验证这一结论。

通过上面的验证,可以知道Student的真实结构如下
Student结构

更复杂的继承结构

屏幕快照 2020-04-26 上午11.16.39

结论:Person对象占用了16个字节的内存空间,Student也占用了16个字节的内存空间。

备注:Student对象的Person_IVARS其实占用了16个字节的内存空间,但是有4个字节的未使用空间,_no变量大小为4个字节,刚好利用这4个内存空间。

我们可以通过malloc_size函数进行验证这个结论

属性和方法

当我们为Person类增加一个属性时,如下:

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

@implementation Person
@end

我们知道@property默认会生成一个带下划线的变量,以及对应属性set和get方法,我们再次通过clang命令编译重写导出后能看到如下代码

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

也即验证了@property可以生成对应的变量。

关于@property生成的方法的细节,我们在后面的文章中再讲解

小结

  • OC对象的本质是结构体
  • 系统分配了16个字节给NSObject对象,但NSObject对象内部只使用了8个字节的空间(64bit环境下,可以通过class_getInstanceSize函数获得)
  • class_getInstanceSize返回的是需要的最小空间,malloc_size返回的是实际分配的内存空间大小

参考和源码

Apple Source Browser - objc

评论