还原OC对象的方法调用轨迹(上)

引言

我们清楚编程语言函数调用的底层逻辑之后,其实再往上了解一些高级语言针对函数调用的包装思想,种类基本上就不统一了。很多语言,比如 C 语言,调用一个方法其实就是跳到内存中的某一点并开始执行一段代码,没有任何动态的特性,因为这一切在编译时就决定好了。 Objective-C 作为一门面向对象语言,它扩展 C 语言加入了面向对象特性和动态消息传递机制,其动态特性的基石便是它的 Runtime 库。在 Objective-C 中,

1
[object fun]

并不会立即执行 fun 这个方法的代码,它是要在运行时给 object 发送一条叫 fun 的消息,这个消息也许会由 object 来处理,也许会被转发给另一个对象。

OC对象的本质

我们平时编写的 OC 代码,其底层实现其实都是 C/C++ 代码。实际上苹果只开放了一小部分源码,所以借助其底层语言可以帮助我们了解验证 OC 的一些本质性问题。OC世界中的“万物之源” NSObject 对象的底层实现是使用 C 语言的结构体。其中只有一个 Class(objc_class*)类型的 isa 指针(64位机器下实际只会占用8个字节,但是由于内存对齐 的原因会被分配16字节空间)。至于继承自 NSObject 的自定义类,除了 isa 指针外还有其成员变量信息。

OC对象的分类

单就某一个类对象而言, OC 中的对象分为三种: instance 对象、 class 对象、 meta-class 对象

instance对象 是通过 alloc 方法实例化出来的对象,每次 alloc 出来的对象都会被分配一个新地址来存储各自的信息,存储信息包括: isa 指针、成员变量的值。
**class对象 *则是针对类本身而言的,每个类在内存中有且只有一份,相应的也是用来存放关于类的信息: isa 指针、 superclass 指针、类的属性信息、对象方法信息、协议信息及成员变量信息等等(注意:这里存放的是成员变量信息并非成员变量的值,比如成员变量的类型、名字等描述信息)。
meta-class对象 跟 class 对象类似,也是针对类本身存在的,内存中有且只有一个该对象,结构也和 class 对象一样都是 Class(objc_class
)类型,只是用途不同,它主要用来放置: isa 指针、 superclass 指针及类的类方法信息。 class 和 meta-class 对象的本质结构都是 struct objc_class 类型的结构体,这个类型的结构体内部设置了存放该类有关所有信息的属性。

isa & superclass

实际上这三种对象也是通过 isa 指针联系起来的:instance 对象的 isa 指针的值对应了 class 对象的地址,class 对象的 isa 指针的值对应了 meta-class 对象的地址,而 meta-class 对象的 isa 指针的值指向了基类的 meta-class 对象的地址(从64bit开始, isa 需要与特定的值 ISA_MASK 做一次位运算才能算出所指向的真实地址,这个 ISA_MASK ,源码中给出了不同设备架构对应的不同的值:arm64:ISA_MASK为0x0000000ffffffff8ULL;x86_64:为0x00007ffffffffff8ULL)
类有继承,在多重继承关系下则通过 superclass 指针将自身与父类及基类联系起来。由于 instance 对象中不含有 superclass 指针,所以实例对象与父类之间的联系均要通过 class 对象和 meta-class 对象中的 superclass 指针建立,这里可以结合这三种类分别存放了何种信息来理解。 class 对象的 superclass 指针指向的是其父类的 class 对象的地址,若其没父类(如:基类)则为 nil ; meta-class 对象的 superclass 指针指向的是父类的 meta-class 对象的地址,若其没父类(如:基类)则指向其 class 对象的地址。可以配合着网上流传的比较经典的图理解上述流程。

所以 instance 实例对象调用自身对象方法的轨迹为:通过 isa 找到自身的 class,方法若不存在,则通过 class 的 superclass 找到父类的 class 对象的该方法,以此类推找下去(若找到基类都没找到方法的实现,这时候就是发挥 OC 语言动态特性的时候了);相似的, class 对象调用类方法的轨迹为:通过 class 对象的 isa 指针找到自身的 meta-class 对象,若其中没有该方法的实现部分,就通过 meta-class 对象的 superclass 指针找到父类的 meta-class 对象的该方法,以此类推找下去。

objc_class结构体源码小撇

这里根据 objc 源码(C++编写)配合分析上述中提到的 objc_class 结构体的内部一些比较熟 悉的变量和函数。该结构体内部第一个变量就是 Class(objc_class*)类型的 isa 指针;同样类型的 superclass 指针;接着是 cache_t 类型(结构体)的 cache 变量,用来缓存该对象曾经调用过的方法的一些信息,比如有16字节的 capacity、 bucket_t 类型(结构体)的 _buckets: _key 和 _imp 等;还有一个 class_data_bits_t 类型(结构体)的 bits 变量,调用该结构体中的 data() 函数得到 class_rw_t 结构体类型的数据(拿bits与FAST_DATA_MASK进行位运算,源码中给出的值是0xfffffffcUL)。 class_rw_t 结构体中含有该类的方法列表 methods(数组)、属性列表 properties(数组)、协议列表 protocols(数组)、其中有个 class_ro_t 结构体类型的 ro 变量。 class_ro_t 结构体包含 instance 对象占用的内存空间大小 instanceSize 变量、类的名称 name 变量、成员变量列表 ivars 。
这里附上objc4的源码地址:https://opensource.apple.com/tarballs/objc4/

参考文献:

第二章:对象、消息、运行期

Effective Objective-C 2.0Matt Galloway(著) 爱飞翔(译)