返回

由一个系统系统调用可以一窥 iOS 的内存分配, isa 与 obj 是如何绑定到一个对象?

IOS

前言

前面在《iOS 对象底层原理之 alloc 分析》中介绍了创建一个对象的关键三步:

  1. 计算类占用的内存大小
  2. 根据计算出来的类占用的内存大小 size,对于 size 进行 16 进制对齐,并在堆中开辟空间
  3. 类(i.e. [NSObject class]) 指向刚开辟的空间,将 Class 类的 isa 指针也指向开辟的空间

但我们知道对象不仅仅包括类成员,它还有实例变量,这些实例变量是怎么放到这个开辟的空间的呢?

对象中还包含了 isa 指针(用来指向类),这个 isa 指针是怎么放到对象的开始位置的呢?

isa & obj 如何绑定到一个对象

编译阶段

我们先从编译阶段讲起,先来看看我们创建了一个对象是如何编译和生成的。

// MyClass.m
@implementation MyClass
- (void)print{
    NSLog(@"Hello, World");
}
@end
  1. 编译器会将这段代码编译成汇编代码,其中最关键的一段如下:
// MyClass.s
.section __TEXT,__text,regular,pure_instructions
.globl _OBJC_CLASS_$_MyClass
_OBJC_CLASS_$_MyClass:
   .quad __objc_empty_cache
   .quad 0                                               // isa 指针
   .long 32                                                // 大小
   .long 16                                                // 实例变量数量
   .long 0                                                // ivar offset
   .long _OBJC_METACLASS_$_MyClass
   .long 0                                                // Cache data pointer
   .long 0
   .long 0
   .long 0
   .long 0
   .long 0
   .asciz "MyClass"                                    // 类名

上诉代码中,我们需要注意的是 _OBJC_CLASS_$_MyClass 这段代码,它表示的是我们在 MyClass.m 文件中定义的类 MyClass 的类对象。

   .long 0                                               // isa 指针

0 指的是 MyClass 类对象的 isa 指针,也就是 MyClass 类的 isa 指针。

我们知道类对象的 isa 指针指向元类,元类的 isa 指针指向 NSObject 类,NSObject 类的 isa 指针指向 _objc_rootClass

   .long 32                                                // 大小

32 指的是 MyClass 类的大小,也就是 MyClass 类占用的内存大小。

   .long 16                                                // 实例变量数量

16 指的是 MyClass 类中实例变量的数量。

   .long 0                                                // ivar offset

0 指的是 MyClass 类中实例变量的偏移量,也就是 MyClass 类中第一个实例变量的偏移量。

   .long _OBJC_METACLASS_$_MyClass

_OBJC_METACLASS_$_MyClass 指的是 MyClass 类的元类对象,也就是 MyClass 类对象的 isa 指针。

运行时

当我们运行程序时,会调用 objc_msgSend 函数来给对象发送消息。

objc_msgSend 函数会首先检查对象的 isa 指针,然后根据 isa 指针找到对象的类对象,再根据类对象的 cache 数据找到对象的方法实现,最后调用方法实现。

如果对象的 isa 指针指向的是元类对象,那么 objc_msgSend 函数会根据元类对象的 cache 数据找到元类的方法实现,最后调用元类的方法实现。

我们知道,对象的 isa 指针是存储在对象的开始位置的,那么 objc_msgSend 函数是如何找到对象的 isa 指针的呢?

内存对齐

在 iOS 中,内存是对齐的,也就是内存地址必须是 16 的倍数。

对象的大小也是对齐的,也就是对象的 size 必须是 16 的倍数。

当我们在堆中开辟空间时,系统会自动将 size 进行 16 进制对齐。

也就是说,如果对象的 size 不是 16 的倍数,那么系统会在对象的前面填充一些字节,使对象的 size 成为 16 的倍数。

填充的字节被称为 对齐字节

对象的布局

对象的布局如下:

+------------------+----------------+
| isa 指针 | 对齐字节 | 实例变量 |
+------------------+----------------+

isa 指针是存储在对象的开始位置的,对齐字节是存储在 isa 指针后面,实例变量是存储在对齐字节后面。

isa & obj 如何绑定到一个对象

当我们在堆中开辟空间时,系统会自动将 size 进行 16 进制对齐。

也就是说,如果对象的 size 不是 16 的倍数,那么系统会在对象的前面填充一些字节,使对象的 size 成为 16 的倍数。

填充的字节被称为 对齐字节

对象的布局如下:

+------------------+----------------+
| isa 指针 | 对齐字节 | 实例变量 |
+------------------+----------------+

isa 指针是存储在对象的开始位置的,对齐字节是存储在 isa 指针后面,实例变量是存储在对齐字节后面。

当我们在 objc_msgSend 函数中通过对象的 isa 指针找到对象的类对象后,我们会根据类对象的 cache 数据找到对象的方法实现,最后调用方法实现。

当我们在 objc_msgSend 函数中通过对象的 isa 指针找到对象的元类对象后,我们会根据元类对象的 cache 数据找到元类的方法实现,最后调用元类的方法实现。

总结

通过上面的分析,我们可以知道 isa 指针是存储在对象的开始位置的,对齐字节是存储在 isa 指针后面,实例变量是存储在对齐字节后面。

当我们在 objc_msgSend 函数中通过对象的 isa 指针找到对象的类对象后,我们会根据类对象的 cache 数据找到对象的方法实现,最后调用方法实现。

当我们在 objc_msgSend 函数中通过对象的 isa 指针找到对象的元类对象后,我们会根据元类对象的 cache 数据找到元类的方法实现,最后调用元类的方法实现。