Objective-C运行时学习笔记-继承体系

其实也不好说是学习笔记还是复习笔记了,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运行时有两个版本 - modernlegacy。两者最大的区别是:在 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_classobjc_object 其实都是 C 语言结构体,整个运行时的底层也都是 C/C++ 实现的。

关于这两个数据结构,网上有很多介绍的文章,可以搜索运行时关键字搜索。

源码中依然保留了 OC legacy 版本的 runtime 部分的数据结构定义,注意不要搞混。legacy 运行时数据结构相关的定义在 objc-runtime-old.h 文件里。

objc_object 表示的类实例,objc_class 则表示类对象。列一下这两个类的成员变量

Untitled

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 是指向哪里呢?元类。

Untitled

# 元类(meta-class)

什么是元类呢?其实就是类对象的类。

但为什么语言机制要涉及元类呢?

回顾上面的对象的表示 objc_object,它里面只有指向所属类的 isa 指针,并没有额外的成员变量保存对象别的信息,比如对象可调用的方法列表和对象包含的属性列表等。想要获取这些信息,必须通过对象的 isa 指针找到所属类,在类中查找方法列表和成员变量。

objc_class 类中的 class_data_bits_t 结构体实例保存着指向方法、属性和协议列表的内存地址。

同理,类方法调用时,通过类的 isa 在元类中获取类方法的实现。

所以元类是必要的,它保存了类的方法和属性列表等信息。

根据我们之前说的类也是对象,那元类的 isa 指针指向哪里呢?答案是指向 NSObject 类的元类。而 NSObject 元类的 isa 指针则指向其自身。

# 父类(superclass)

objc_class 中有 superclass 成员变量,superclass 就是指向当前类的父类。这个概念很理解就不赘述了。

# 基于运行时的继承体系

基于我们上面对元类和父类的分析,Objective-C 的整体继承体系如下:

Untitled

看起来是比 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 本身有动态特性的方法,子类继承了父类的能力,所有子类也有动态特性的方法。

参考地址:

  1. Objective-C Runtime Programming Guide (opens new window)
  2. Programming with Objective-C (opens new window)
  3. What is a meta-class in Objective-C? (opens new window)
  4. 从 NSObject 的初始化了解 isa (opens new window)
  5. JS学习笔记-原型链&类&构造函数

关注我的微信公众号,我在上面会分享我的日常所思所想。