Objective-C 的运行时系统(Runtime)有两个版本:“Modern” 和 “Legacy”。我们目前使用的 Objective-C 2.0 采用的是现行(Modern)版本的 Runtime 系统,它只能运行于 iOS 和 macOS 10.5 之后的 64 位程序中,而 macOS 较老的 32 位程序仍采用 Objective-C 1.0 早期(Legacy)版本的 Runtime 系统。
在上一篇文章《Objective-C 1.0 中类与对象的定义》中,我们介绍了早期 Objective-C 1.0 中类与对象的定义,本文以最新的 Objective-C Runtime 源码 objc4-750.1.tar.gz 进行阅读和分析 Objective-C 2.0 中类与对象的定义。
需要说明的是,本文只是简单罗列出了 Objective-C 2.0 中类与对象的相关定义,仅作为参考,并没有做深入的分析,因为这方面的文章已经有很多了,详见文末的参考文档。
注:文中的相关示意图均来自霜神(halfrost)的这篇博文,在此不胜感激。
Class 和 id
在 objc-private.h 文件中:
Class
被定义为指向struct objc_class
的指针;id
被定义为指向struct objc_object
的指针;
1 | typedef struct objc_class *Class; |
其中,objc_object
结构体的定义如下:
1 | struct objc_object { |
objc_object
结构体用于初始化 Objective-C 中类的实例对象,它包含一个名为 isa
的成员变量,其类型为 union isa_t
共用体,isa_t
中有一个 Class
类型的指针 cls
,用于指向实例对象所对应的类,关于 isa
下面会详细分析。
在 objc-runtime-new.h 文件中,objc_class
结构体的定义如下:
1 | struct objc_class : objc_object { |
我们可以发现,objc_class
继承于 objc_object
,也就是说,在 Objective-C 中,类本身也是一个对象,其 “isa 指针” 指向对应的“元类”(Meta Class),superclass
指针指向父类,cache
为调用过的方法缓存,而 bits
保存着这个类的属性(成员变量)、实例方法链表、以及遵循的协议等信息,示意图如下:
cache_t
方法缓存的类型 cache_t
的定义如下:
1 | struct cache_t { |
其中,bucket_t *_buckets
是一个散列表,用来存储 Method
方法列表,而 bucket_t
结构体定义如下,包含了一个 unsigned long
类型的 _key
和一个 IMP
类型的 _imp
,存储了指针与 IMP 的键值对。IMP
是一个函数指针,指向了一个方法的具体实现。
1 | struct bucket_t { |
cache_t
中另外两个变量 _mask
和 _occupied
,它们的类型为 mask_t
,定义如下,其实是一个 unsigned int
。
1 |
|
_mask
和 _occupied
对应于 vtable
:
_mask
:分配用来缓存 bucket 的总数。_occupied
:表明目前实际占用的缓存 bucket 的个数。
cache
的作用主要是对方法调用的性能进行优化。通俗地讲,每当实例对象接收到一个消息时,它不会直接在其isa
指向的类(或类的isa
指向的父类)的方法列表中遍历查找能够响应消息的方法实现,因为这样效率太低了,而是优先在cache
中查找。Runtime 系统会把被调用过的方法存到该类对象的cache
中(理论上讲一个方法如果被调用,那么它有可能今后还会被调用),下次查找的时候效率更高。– 摘自这里
class_data_bits_t
objc_class
结构体中最复杂的是 bits
成员变量,其类型为 class_data_bits_t
,定义如下:
1 | struct class_data_bits_t { |
从 objc_class
的定义中我们可以发现,objc_class
的 data()
方法直接调用 class_data_bits_t
的 data()
方法,最终是返回 class_rw_t *
。
class_rw_t
Objective-C 的类的属性、方法、以及遵循的协议在 2.0 版本之后都放在 class_rw_t
结构体中,其定义如下:
1 | struct class_rw_t { |
class_rw_t
提供了运行时对类拓展的能力,而 class_ro_t
存储的大多是类在编译时就已经确定的信息。二者都存有类的方法、属性(成员变量)、协议等信息,不过存储它们的列表实现方式不同。
class_rw_t
中使用的 method_array_t
, property_array_t
, protocol_array_t
都继承自 list_array_tt<Element, List>
, 它可以不断扩张,因为它可以存储 list 指针,内容有三种:(1)空;(2)一个 entsize_list_tt
指针;(3)entsize_list_tt
指针数组。
class_rw_t
的内容是可以在运行时被动态修改的,可以说运行时对类的拓展大都是存储在这里的。
class_ro_t
通过源码,我们可以知道,objc_class
结构体包含了 class_data_bits_t
,class_data_bits_t
存储了 class_rw_t
的指针,而 class_rw_t
结构体又包含 class_ro_t
的指针。
在 class_rw_t
中,class_ro_t
是一个指向常量的指针,存储了编译器决定了的属性、方法和遵守协议。class_ro_t
的定义如下,
1 | struct class_ro_t { |
其中,class_ro_t
里的 method_list_t
, ivar_list_t
, property_list_t
结构体都继承自 entsize_list_tt<Element, List, FlagMask>
,而 protocol_list_t
与前三个不同,它存储的是 protocol_t *
指针列表,实现比较简单。
entsize_list_tt
实现了 non-fragile
特性的数据结构,定义如下:
1 | template <typename Element, typename List, uint32_t FlagMask> |
关于 entsize_list_tt
的分析和 Non Fragile ivars
特性,详见这篇文章。
假如苹果在新版本的 SDK 中向
NSObject
类增加了一些内容,NSObject
的占据的内存区域会扩大,开发者以前编译出的二进制中的子类就会与新的NSObject
内存有重叠部分。Non Fragile ivars
特性在编译期会给instanceStart
和instanceSize
赋值,确定好编译时每个类的所占内存区域起始偏移量和大小,这样只需将子类与基类的这两个变量作对比即可知道子类是否与基类有重叠,如果有,也可知道子类需要挪多少偏移量。
综上,objc_class
中 class_data_bits_t
,class_rw_t
和 class_ro_t
的关系如下:
关于 Objective-C 2.0 中方法结构的深入解析,详见这篇文章。
isa
在上一篇文章中,我们介绍了,对象是某一个类的实例,在 Objective-C 中,类也被设计成一个对象,它是其对应的元类(Meta Class)的实例,而元类其实也是一个对象,它的类型是根元类(Root Meta Class),根元类的类型是它自己,它们的继承关系和 isa 指向关系如下图所指示:
此外,类对象和元类对象是全局唯一的,而对象是可以在运行时创建无数个的。在 main 方法执行之前,从 dyld 到 Runtime 加载期间,类对象和元类对象在此时被创建。
在 Objective-C 1.0 中,isa
只是一个指向 Class
的指针,而在 2.0 中,objc_object
中的 isa
是一个 isa_t
类型的共用体(union),在 objc-private.h 文件中定义如下:
1 |
|
其中,ISA_BITFIELD
宏定义了 isa_t
在不同处理器架构(arm64
和 x86_64
)下的结构,每个字段的含义如下表:
变量名 | 含义 |
---|---|
indexed | 0 表示普通的 isa 指针,1 表示使用优化,存储引用计数 |
has_assoc | 表示该对象是否包含 associated object,如果没有,则析构时会更快 |
has_cxx_dtor | 表示该对象是否有 C++ 或 ARC 的析构函数,如果没有,则析构时更快 |
shiftcls | 类的指针 |
magic | 固定值为 0xd2,用于在调试时分辨对象是否未完成初始化。 |
weakly_referenced | 表示该对象是否有过 weak 对象,如果没有,则析构时更快 |
deallocating | 表示该对象是否正在析构 |
has_sidetable_rc | 表示该对象的引用计数值是否过大无法存储在 isa 指针 |
extra_rc | 存储引用计数值减一后的结果 |
示意图:
关于 isa
的更多分析和 Tagged Pointer
的概念,详见这篇文章和这篇文章。
SEL
在 objc.h 中,定义了 SEL
为指向 struct objc_selector
的指针:
1 | /// An opaque type that represents a method selector. |
在 objc4 源码中,我们找不到 objc_selector
结构体的具体定义,我们可以把 SEL
理解为就是个映射到方法名的 C 字符串。
此外,不同类中相同名字的方法所对应的方法选择器是相同的,即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择器。
我们可以通过编译器命令 @selector()
或者 Runtime 系统提供的 sel_registerName
函数来获得一个 SEL
类型的方法选择器。
IMP
在 objc.h 中,IMP
的定义如下:
1 | /// A pointer to the function of a method implementation. |
根据宏判断,我们可以知道,在 2.0 中,IMP
为:
1 | typedef void (*IMP)(void /* id, SEL, ... */ ); |
IMP
是一个函数指针,指向方法最终的函数实现,它的参数与 objc_msgSend
函数的参数类型相同,关于 Objective-C 的“方法调用”与“消息转发”,我们这里不再赘述。
Method
在 objc-private.h 中,Method
被定义为一个指向 method_t
结构体的指针:
1 |
|
method_t
结构体在 objc-runtime-new.h 中定义:
1 | struct method_t { |
其中 MethodListIMP
在 objc-ptrauth.h 中声明,可以把它理解为就是 IMP
函数指针:
1 |
|
所以,Method
其实存储了方法名,方法类型和方法实现:
- 方法名类型为
SEL
,相同名字的方法即使在不同类中定义,或者参数类型不同,但它们的方法选择器是相同的。 - 方法类型
types
是个 char 指针,其实存储着方法的参数类型和返回值类型。 imp
指向了方法的函数实现,本质上是一个函数指针。
Ivar
在 objc-private.h 中,Ivar
被定义为一个指向 ivar_t
结构体的指针:
1 |
|
ivar_t
结构体在 objc-runtime-new.h 中定义:
1 | struct ivar_t { |
Ivar
用于表示类中实例变量(成员变量)的类型,ivar_t
中定义了实例变量的名字和类型。
Property
我们知道,@property
用于声明类中的属性,在 Runtime 系统中,objc_property_t
是一个指向objc_property
结构体的指针,在 objc-private.h 中定义:
1 |
|
property_t
结构体在 objc-runtime-new.h 中定义:
1 | struct property_t { |
entsize_list_tt
此外,在 objc-runtime-new.h 中也定义了 method_list_t
,ivar_list_t
,property_list_t
,它们都继承于 entsize_list_tt
,如下:
1 | // Two bits of entsize are used for fixup markers. |
entsize_list_tt
的声明如下:
1 | template <typename Element, typename List, uint32_t FlagMask> |
它们将在上述提到的 class_ro_t
的结构体中使用。
Category
在 objc-private.h 中,Category
被定义为一个指向 category_t
结构体的指针:
1 |
|
category_t
结构体在 objc-runtime-new.h 中定义:
1 | struct category_t { |
category_t
存储了分类中可以拓展的实例方法、类方法、协议、实例属性和类属性。在 App 启动时,Runtime 加载完类后,会通过调用 attachCategories
函数进行向类中添加 Category
的工作。原理就是向 class_rw_t
中的 method_array_t
, property_array_t
, protocol_array_t
数组中分别添加 method_list_t
, property_list_t
, protocol_list_t
指针。
Protocol
在 objc-runtime-new.h 中定义了 protocol_t
结构体:
1 | struct protocol_t : objc_object { |
protocol_t
继承于 objc_object
,其中定义了协议中声明的实例方法,类方法,可选实例方法,可选类方法,实例属性和类属性等。此外,由于 Swift 还支持 Protocol
多继承,所以需要 protocols
数组来做兼容。
protocol_list_t
定义如下:
1 | struct protocol_list_t { |
总结
通过两篇文章的整理,我们可以用如下两张图(来源)来分别表示 Objective-C 1.0 和 2.0 版本中类和对象的定义,及相关数据结构的关系: