引言
在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有更深入的认识,在实际开发中能够更加灵活地运用这些知识。