本文使用的 runtime 版本为 objc4-706

众所周知,在 Objective-C 中,如下的消息发送:

[receiver message];

会被编译器转换为:

objc_msgSend(receiver, @selector(message));

这样,实际的函数调用在运行时(runtime)才能确定,即所谓的动态绑定

要了解 objc_msgSend 的具体实现,需要先了解 Objective-C 运行时中对象,类和方法的实现以及它们的关系。

对象

在 Objective-C 中,有一个类型 id,用来表示指向对象的指针,它的定义能在 Objective-C 运行时的公开头文件 objc.h 中找到:

/// A pointer to an instance of a class.
typedef struct objc_object *id;

显然 objc_object 结构体就是 Objective-C 运行时中用来表示对象的数据结构了,它的定义也能在 objc.h 中看到:

/// Represents an instance of a class.
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

Class 类型代表的肯定是类,isa 这个名字也很直观(is a some class)。因为结构体只是一个内存排布的约定,所以任何第一个成员是 Class 类型的结构体都可以视作为对象。

类和元类

objc.h 中,可以看到 Class 的定义:

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

所以 Class 准确来说是指向类的指针,而 objc_class 结构体就是类真正的实现了。在公开头文件 runtime.h 中,可以看到 objc_class 结构体的定义:

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

注意到 objc_class 结构体的第一个成员也是 Class isa,这样它也能被当作是一个对象。接下来是 Class super_class,用来指向父类。再者可以看到 objc_class 结构体有一个叫 methodLists 的成员,用来存放类的实例方法。

现在就有了两个问题,类的 isa 指向什么?类的类方法存放在哪里呢?这两个问题的答案都是「元类(meta-class)」。

每个类都有一个对应的元类,类的 isa 指向其对应的元类,对应的元类中存放着类的类方法。显然又有了新的问题,元类的类型也是 Class,那它的 isasuper_class 指向哪里呢?元类的 super_class 很自然的指向其对应类的父类的元类,而 isa 则指向「根类(root class)」所对应的元类。

这篇文章对元类做了非常好的解释,并且有一张很好的对对象、类和元类关系的图示:

可以看到,根类的 super_class 指向 nil,根类对应的元类的 super_class 指向根类。

方法

objc_class 结构体中可以看到存放方法的成员类型是 objc_method_list,它的定义能在 runtime.h 中找到:

struct objc_method_list {
    struct objc_method_list *obsolete                        OBJC2_UNAVAILABLE;

    int method_count                                         OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

很显然,objc_method 结构体就是表示方法的数据结构,runtime.h 中对 Method 类型的定义也证明了这点:

/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;

同样在 runtime.h 中,可以找到 objc_method 结构体的定义:

struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

其中 SEL 的定义可以在 objc.h 中找到:

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

实际上,objc_selector 结构体根本就没有实现,这里就是它唯一存在的地方了。所以 SEL 可以看作是 void *

method_types 这个成员是方法的编码,具体的规则在官方文档里有一些说明。

IMPobjc.h 中的定义是这个样子的:

typedef id (*IMP)(id, SEL, ...); 

所以 IMP 其实是一个函数指针,第一个参数是一个对象,第二个参数是一个方法名。这两个参数其实就对应着 self_cmd

这么看来,方法的表示还是很清晰的,就是方法名、方法的类型和具体实现。

消息发送

首先看一下公开头文件 message.hobjc_msgSend 的声明:

id objc_msgSend(id self, SEL _cmd, ...);

然后可以继续 objc_msgSend 函数的大概逻辑了:

  1. 检查 self 是否为 nil,是的话直接返回 nil,这就是为什么可以给 nil 发消息;
  2. 通过 selfisa 获取到类(或元类);
  3. 使用 _cmd 在类(或元类)的方法中进行查找,没有找到的话,通过类(或元类)的 super_class 继续查找;
  4. 找到了对应的方法,调用其 IMP
  5. 没有找到对应的方法,进入动态方法解析甚至之后的消息转发过程。

每次都要查找,对一个会被调用成千上万次的函数来说,开销实在是太大了。所以就要给它加上缓存,也就是 objc_class 结构体中的 struct objc_cache *cache。可以在 runtime.h 中看到 objc_cache 结构体的信息:

typedef struct objc_cache *Cache                             OBJC2_UNAVAILABLE;

#define CACHE_BUCKET_NAME(B)  ((B)->method_name)
#define CACHE_BUCKET_IMP(B)   ((B)->method_imp)
#define CACHE_BUCKET_VALID(B) (B)
#ifndef __LP64__
#define CACHE_HASH(sel, mask) (((uintptr_t)(sel)>>2) & (mask))
#else
#define CACHE_HASH(sel, mask) (((unsigned int)((uintptr_t)(sel)>>3)) & (mask))
#endif
struct objc_cache {
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
    unsigned int occupied                                    OBJC2_UNAVAILABLE;
    Method buckets[1]                                        OBJC2_UNAVAILABLE;
};

所以 objc_msgSend 加上缓存的逻辑后:

  1. 检查 self 是否为 nil,是的话直接返回 nil
  2. 检查缓存中是否有对应的 _cmd,有的话,调用其 IMP 并返回;
  3. 缓存未命中,通过 selfisa 获取到类(或元类);
  4. 使用 _cmd 在类(或元类)的方法中进行查找,没有找到的话,通过类(或元类)的 super_class 继续查找;
  5. 找到了对应的方法,将其存入缓存,调用其 IMP
  6. 没有找到对应的方法,进入动态方法解析甚至之后的消息转发过程。

总结

知道了 Objective-C 中对象和类是如何表示和关联后,objc_msgSend 的原理和逻辑还是比较清晰的。之所以 Objective-C 中对象和类是如此表示,其实是一开始是受到 Smalltalk 的影响,很多东西都是直接照搬过来(甚至消息发送的语法,nil 等)。

上面的代码中都可以看到 OBJC2_UNAVAILABLE 这个宏,宏如其名,标上这个宏的东西其实是在 Objective-C 2.0 也就是我们现在所使用的 Objective-C 中,是过时的。现在的实现在细节方面已经有了较大的变化,但整体的思路并没有改变,所以这篇文章中的逻辑在现在的实现中并没有改变。