引言
在Objective - C的世界里,Runtime(运行时系统)犹如幕后的神秘操控者,它赋予了这门语言动态特性,使得很多在编译时无法确定的操作能够在运行时灵活处理。了解Runtime的底层原理,对于深入掌握Objective - C编程、优化代码性能以及解决复杂问题都有着至关重要的意义。本文将深入剖析Runtime中的几个核心概念,包括isa指针、class的结构以及方法缓存。
一、isa指针详解
1.1 isa指针的基本概念
在Objective - C里,几乎所有的对象都是objc_object结构体的实例,而objc_object结构体中最重要的成员就是isa指针。isa指针的主要作用是指向对象所属的类,通过这个指针,对象能够知道自己应该调用哪个类的方法。
下面是objc_object结构体的简化定义:
1 | struct objc_object { |
这里的Class实际上是一个指向objc_class结构体的指针,也就是说isa指针指向的是一个objc_class类型的实例,这个实例代表了对象所属的类。
1.2 isa的本质
在早期的Objective - C版本中,isa 确实只是一个简单的指针,直接指向对象所属的类。但从64位系统开始,isa 不再仅仅是一个单纯的指针,而是被设计成了一个 isa_t 类型的共用体(union)。共用体的特点是所有成员共享同一块内存空间,这使得 isa 可以在有限的内存空间里存储更多的信息。
isa_t 共用体的简化定义如下:
1 | union isa_t { |
1.3 isa中存储的内容
- nonpointer:这是一个标志位,占1位。当它的值为 0 时,表示
isa是一个普通的指针,仅存储类的地址;当值为 1 时,意味着isa是一个非指针类型,除了存储类的地址外,还会存储其他额外信息。 - has_assoc:同样是一个标志位,占1位。用于标记对象是否有关联对象。如果对象使用了
objc_setAssociatedObject方法关联了其他对象,这个标志位会被置为 1。 - has_cxx_dtor:1位的标志位,用于判断对象是否有 C++ 析构函数。若对象有 C++ 析构函数,该标志位为 1,在对象销毁时需要调用相应的析构函数。
- shiftcls:占据 33 位,这部分存储的是类的指针值。由于内存对齐的原因,类的地址会有一定的偏移,通过右移操作可以得到真正的类地址。
- magic:占 6 位,用于调试时判断对象是否已经完成初始化。
- weakly_referenced:1位标志位,用来标记对象是否被弱引用指向。如果对象被弱引用引用,该标志位为 1。
- deallocating:1位标志位,指示对象是否正在进行析构操作。
- has_sidetable_rc:1位标志位,判断对象的引用计数是否存储在
sidetable中。当对象的引用计数过大,extra_rc无法存储时,会将部分引用计数存储在sidetable中,此时该标志位为 1。 - extra_rc:占据 19 位,存储对象的引用计数。当对象的引用计数增加时,
extra_rc的值会相应增加。
1.4 isa指针的指向规则
实例对象的isa指针
实例对象的isa指针指向其所属的类。例如,当我们创建一个Person类的实例person时,person对象的isa指针就指向Person类。
类对象的isa指针
类对象的isa指针指向元类(meta - class)。元类是一种特殊的类,它存储了类方法的信息。当我们调用一个类方法时,实际上是通过类对象的isa指针找到对应的元类,然后在元类中查找并调用相应的方法。
元类的isa指针
元类的isa指针指向根元类(root meta - class)。根元类是所有元类的基类,它的isa指针指向自己,形成一个闭环。
1.5 isa指针的走位图
通过一个简单的示例和对应的走位图能更直观地理解isa指针的指向关系。假设我们有一个继承自NSObject的Person类:
1 | @interface Person : NSObject |
对应的isa指针走位图如下:

二、class的结构
2.1 class的基本结构
在Runtime中,类是通过objc_class结构体来表示的。下面是objc_class结构体的简化定义:
1 | struct objc_class { |
isa指针
前面已经详细介绍过,它指向类的元类,用于确定类方法的查找路径。
superclass指针
superclass指针指向父类,通过这个指针可以实现类的继承关系。当在当前类中找不到某个方法时,Runtime会沿着superclass指针的路径到父类中继续查找。
cache(方法缓存)
cache是一个cache_t类型的结构体,用于缓存已经调用过的方法,以提高方法查找的效率。后续会详细介绍方法缓存的相关内容。
bits(类的数据信息)
bits是一个class_data_bits_t类型的结构体,它存储了类的具体数据信息,包括方法列表、属性列表、协议列表等。
2.2 从bits中获取类的信息
通过bits可以获取类的各种信息,例如方法列表、属性列表等。以下是一个简单的示例代码,展示如何获取类的方法列表:
1 | #import <objc/runtime.h> |
在这个示例中,我们使用class_copyMethodList函数从Person类的bits中获取方法列表,并遍历打印出每个方法的名称。
三、方法缓存(cache_t)
3.1 方法缓存的作用
在Objective - C中,方法调用是一个相对复杂的过程,需要在类及其父类的方法列表中查找对应的方法。为了提高方法调用的效率,Runtime引入了方法缓存机制。当一个方法被调用时,Runtime会将该方法的信息(包括方法名和方法实现)缓存到类的cache中。下次再调用相同方法时,会直接从缓存中查找,避免了重复的方法查找过程,从而显著提高了方法调用的速度。
3.2 cache_t结构体的结构
cache_t结构体的简化定义如下:
1 | struct cache_t { |
_buckets
_buckets是一个指向bucket_t结构体数组的指针,bucket_t结构体存储了方法的具体信息,包括方法名(SEL)和方法实现(IMP)。以下是bucket_t结构体的简化定义:
1 | struct bucket_t { |
_mask
_mask是一个掩码,用于确定_buckets数组的大小。_mask的值通常是2^n - 1的形式,这样在进行哈希计算时可以更高效。
_occupied
_occupied表示当前_buckets数组中已经占用的桶(bucket)的数量。
3.3 cache_t是如何缓存的
缓存插入
当一个方法被调用时,首先会计算该方法选择器(SEL)的哈希值。通常使用 SEL 的值与 _mask 进行按位与运算(hash = _sel & _mask),以此来确定该方法在 _buckets 数组中的初始位置。
如果计算得到的位置为空,那么直接将该方法的信息(SEL 和 IMP)存储到这个 bucket 中。但如果该位置已经被占用,就会采用开放寻址法(通常是线性探测)来寻找下一个可用的位置。线性探测就是依次检查下一个位置,直到找到一个空的 bucket 为止。
以下是简化的插入逻辑伪代码:
1 | hash = _sel & _mask; |
缓存查找
当再次调用相同方法时,同样会先计算该方法选择器(SEL)的哈希值,使用相同的哈希计算方式(hash = _sel & _mask)得到在 _buckets 数组中的初始位置。
然后从这个位置开始检查 bucket 中的 SEL 是否与要查找的 SEL 相等。如果相等,就表示找到了对应的方法,直接调用该 bucket 中的 IMP。如果不相等,就按照线性探测的方式继续检查下一个位置,直到找到匹配的 SEL 或者遍历完整个 _buckets 数组。
以下是简化的查找逻辑伪代码:
1 | hash = _sel & _mask; |
缓存扩容
当 _occupied 的值达到一定阈值(通常是 _mask 的 3/4)时,cache_t 会进行扩容操作。扩容时会创建一个更大的 _buckets 数组,新数组的大小一般是原来的两倍。
然后将原数组中的所有 bucket 重新哈希到新数组中。重新哈希的过程就是对原数组中的每个 bucket 重新计算哈希值,然后根据新的 _mask 确定在新数组中的位置,并将其存储到新位置。
以下是简化的扩容逻辑伪代码:
1 | oldBuckets = _buckets; |
四、总结
Objective - C的Runtime系统是一个复杂而强大的机制,其中isa指针、class的结构以及方法缓存是Runtime的核心组成部分。isa指针确定了对象、类和元类之间的关系,其本质是一个 isa_t 共用体,存储了丰富的信息,是方法调用的基础;class的结构存储了类的各种信息,包括方法、属性和协议等;方法缓存则提高了方法调用的效率,减少了不必要的方法查找开销。深入理解这些概念,有助于我们更好地掌握Objective - C的动态特性,优化代码性能,解决开发中遇到的各种问题。
希望通过本文的介绍,你能对Objective - C Runtime有更深入的认识,在实际开发中能够更加灵活地运用这些知识。