Objective-C 小记(1)Messaging

众所周知,在 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)」所对应的元类。

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

class diagram.jpg

可以看到,根类的 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 中,是过时的。现在的实现在细节方面已经有了较大的变化,但整体的思路并没有改变,所以这篇文章中的逻辑在现在的实现中并没有改变。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容