其实也不好说是学习笔记还是复习笔记了,Objective-C 这语言从毕业开始一直用,直到 18 年前后换成了 Swift. 前几天看 JS 原型链的时候发现,这个和 OC 语言的继承体系好像啊,趁着这个机会回顾一下 OC 语言中的继承实现。
说起 OC 的继承实现就不能不提到支持 OC 这门语言运行的机制 runtime
. 网上相关的文章已经介绍烂了,为了方便我自己之后回顾,我还是从自己理解的角度再聊一下 runtime
的相关概念,同时也和别的语言进行一下对比。
# Objective-C语言介绍
OC 语言是 C 语言的超集,在 C 语言的基础上提供面向对象的能力和动态运行时。和 JavaScript
相比,OC 是一门编译型的语言,OC 编译完之后会生成一个 mach-o 文件,作为可执行文件。
我自己理解 OC 其实并不算是静态类型语言,虽然在写 OC 代码的时候会声明数据的类型,但是编译器并不会严格的检查数据类型。比如我们在 OC 里面这样写代码,编译器并不会报错,而只是给个警告 ⚠️
NSString *str = @1;
//Warning : Incompatible pointer types initializing 'NSString *' with an expression of type 'NSNumber *'
本质上是因为对象本质都是运行时创建的(运行时系统提供了创建方法),所以仅仅凭编译没有办法确定数据的真正类型。从这点上来说 OC 也不算是强类型语言,相对于 Swfit 来说,OC 类型系统对类型的检查并不足够严格。至于 JavaScript 就更谈不上强类型语言了,我脑海里突然冒出一个想法,JavaScript 有类型检查系统吗…
弱/强类型指的是语言类型系统的类型检查的严格程度。比如强类型语言中不允许有任何的隐式类型转换,而弱类型语言则允许任意的数据隐式类型转换。 静态/动态语言区分标准,应该就是类型检查的时机,编译时检查就是静态语言,运行时检查就是动态语言。
# 运行时系统(runtime)简介
作为动态类型的语言 Objective-C 语言尽可能地将许多决策从编译时间和链接时间推迟到运行时。只要有可能,它会以动态方式执行操作。这意味着该语言不仅需要编译器,还需要运行时系统来执行编译后的代码。运行时系统充当了Objective-C语言的一种操作系统;它是使语言正常工作的基础。
正是运行时机制的存在,让 OC 中的很多动态特性成为可能。
Objective-C运行时有两个版本 -
modern
和legacy
。两者最大的区别是:在modern
时中,如果您更改了类中实例变量的布局,则不必重新编译继承自它的类。原因是底层的objc_class
修改了数据结构实现。有意思的是,Objective-C 2.0 版本是 2012 年推出的,国内 2012 年移动互联网开始快速发展,很多人也差不多这时候开始入行,但是直到 17、18 年很多人分析
runtime
还是用的legacy
版本的数据结构。
# 进入运行时
从上层语言层面看来,Objective-C 中所有的类都需要继承自 NSObject (opens new window) 类,即 NSObject
是 OC 类继承体系中的根类,为什么所有的 OC 类都要继承 NSObject 呢?因为它提供了所有类的动态特性。至于它是如何做到提供类的的动态特性的?介绍完继承体系相信你就能明白。
我们从 NSObject
类这里进入了冰层之下的 OC runtime 体系,看看运行时是如何支撑 OC 各种动态特性的,这篇文章里,我们主要还是想看看 OC 的继承体系是如何做到的。
苹果在自己的 Apple Source (opens new window) 里提供了
runtime
的源码,目前 macOS 13.5 带的 runtime 版本是objc4-876
。我在网上找了一份能编译 runtime 的版本 (opens new window),这样方便我们调试。
# 运行时数据结构
NSObject 的底层实现是什么样子?NSObject.h
文件中的 NSObject
定义如下:
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
NSObject
的定义中只包含了成员变量 isa
。这里面的 Class
关键字是表示类,而在底层实现中 Class
关键字就是 stuct objc_class *
,并没有魔法。所以相当于 NSObject
类只是在 objc_class
外面包了一层而已。
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class
接着引出到运行时底层最基础的数据结构 objc_class
以及 objc_object
。
# objc_class & objc_object
翻看运行时源码会发现,objc_class
和 objc_object
其实都是 C 语言结构体,整个运行时的底层也都是 C/C++ 实现的。
关于这两个数据结构,网上有很多介绍的文章,可以搜索运行时关键字搜索。
源码中依然保留了 OC
legacy
版本的runtime
部分的数据结构定义,注意不要搞混。legacy
运行时数据结构相关的定义在objc-runtime-old.h
文件里。
objc_object
表示的类实例,objc_class
则表示类对象。列一下这两个类的成员变量
而 objc_class
是继承 objc_object
的。
struct objc_class : objc_object {
isa_t isa; //在 objc_object 中
Class superclass;
cache_t cache;
class_data_bits_t bits;
};
这里涉及到 isa_t
这个数据类型,这个数据类型内部比较复杂,这里不做具体介绍,简单说就是存储一个指针,指向了当前对象所属类,所以本质上除了类实例之外,类也是对象。
那问题来了,如果 objc_object
中的 isa
是指向实例所属类的话,objc_class
中的 isa
是指向哪里呢?元类。
# 元类(meta-class)
什么是元类呢?其实就是类对象的类。
但为什么语言机制要涉及元类呢?
回顾上面的对象的表示 objc_object
,它里面只有指向所属类的 isa
指针,并没有额外的成员变量保存对象别的信息,比如对象可调用的方法列表和对象包含的属性列表等。想要获取这些信息,必须通过对象的 isa
指针找到所属类,在类中查找方法列表和成员变量。
objc_class
类中的class_data_bits_t
结构体实例保存着指向方法、属性和协议列表的内存地址。
同理,类方法调用时,通过类的 isa
在元类中获取类方法的实现。
所以元类是必要的,它保存了类的方法和属性列表等信息。
根据我们之前说的类也是对象,那元类的 isa
指针指向哪里呢?答案是指向 NSObject
类的元类。而 NSObject
元类的 isa
指针则指向其自身。
# 父类(superclass)
objc_class
中有 superclass
成员变量,superclass
就是指向当前类的父类。这个概念很理解就不赘述了。
# 基于运行时的继承体系
基于我们上面对元类和父类的分析,Objective-C 的整体继承体系如下:
看起来是比 JS 的原型链 (opens new window)还要复杂的存在,JS 似乎没有元类这个概念,Objective-C 使用两个指针(isa
& superclass
)完成了类、元类和父类查找,而 JS 只用一个 __proto__
属性来做这件事。
# 基于运行时的消息查找体系
Objective-C 的方法查找机制也是以这个体系为框架进行查找,当我们调用实例方法的话,运行时会先通过isa指针找到实例所属类,在类的方法列表找有没有这个方法。如果在方法列表中找不到该选择器,objc_msgSend
会继续跟踪指向父类(superclass)的指针,并尝试在其方法列表中查找该选择器。连续的失败会导致 objc_msgSend
沿着类层次结构向上查找,直到达到 NSObject
类为止。如果 NSObject
类依然没有此方法,则会走消息转发机制。
# 总结
Objective-C
语言是架在在运行时之上的,运行时是我们日常编写的各种代码的粘合剂,没有运行时机制,就谈不上 Objective-C
的继承体系。整个 Objective-C
的继承体系全部都建立在运行时之上。
回到之前的一个小问题,NSObject 是如何提供动态特性的?要知道自己写的类是只负责业务相关的逻辑,并不关心动态特性,但这些类都有动态特性,比如可以调用 为 -isKindOfClass: 之类的方法,本质上就是因为我们写的类都继承了 NSObject,当想自己的类实例发送消息的时候,这些消息发到 NSObject 去进行处理,即 NSObject 本身有动态特性的方法,子类继承了父类的能力,所有子类也有动态特性的方法。
参考地址:
- Objective-C Runtime Programming Guide (opens new window)
- Programming with Objective-C (opens new window)
- What is a meta-class in Objective-C? (opens new window)
- 从 NSObject 的初始化了解 isa (opens new window)
- JS学习笔记-原型链&类&构造函数
关注我的微信公众号,我在上面会分享我的日常所思所想。