Objective-C Runtime之Messaging

已下大部分内容参考于
Objective-C Runtime
杨萧玉的博客 Objective-C Runtime
深入理解Objective-C的Runtime机制
Objc Runtime笔记 by 戴铭
Objective-C对象之类对象和元类对象(一)

什么是runtime?

Runtime(运行时),是一套底层的 C 语言 API,其为 iOS内部的核心 之一,我们平时编写的 OC代码,底层都是基于它来实现的 。
Objective-C 对象可以用C语言中的结构体表示,而方法(methods)可以用C函数实现。这些结构体和函数被runtime函数封装后,Objective-C程序员可以在程序运行时创建,检查,修改类,对象和它们的方法。
除了封装,Objective-C runtime库也负责找出方法的最终执行代码。当程序执行[object doSomething]时,不会直接找到方法并调用。而是会发送一条消息(message)给对象(在这儿,我们通常叫它接收者),如下例:

Person *person = [[Person alloc] init];
[person eat];
// 底层运行时会被编译器转化为:
objc_msgSend(person, @selector(eat))
// 如果其还有参数比如:
[person eat:(id)arg...];
// 底层运行时会被编译器转化为:
objc_msgSend(person, @selector(eat), arg1, arg2, ...)

在上面代码中,person对象就是接收者,系统会给person对象发送一个@selector(eat)消息。然后person对象(接收者)根据消息名'eat'去Person类中找eat方法的实现,然后再执行eat方法(具体怎么执行,接下来会细说)。
这种消息传递机制源于Smalltalk,(Objective-C根据Smalltalk发展而来。)
Objective-C 扩展了C语言,将Smalltalk的消息传递机制加入到了C中,并加入了面向对象特性。
Runtime是开源的,可以去Apple的Open Source下载;

Objective-C

Objective-C 是一个动态语言,这意味着它不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象、进行消息传递和转发。理解 Objective-C 的 Runtime 机制可以帮我们更好的了解这个语言,适当的时候还能对语言进行扩展,从系统层面解决项目中的一些设计或技术问题。了解 Runtime ,要先了解它的核心 - 消息传递 (Messaging)。

Messageing

I'm sorry that I long ago coined the term "objects" for this topic because it gets many people to focus on the lesser idea.
The big idea is "messaging" - that is what the kernal of Smalltalk/Squeak is all about (and it's something that was never quite completed in our Xerox PARC phase).

Alan Kay反复强调消息传递(message-passing)是Smalltalk最重要的部分,objects(面向对象)只是lesser idea,messaging(消息传递)才是big idea

先来看看Runtime的一些数据结构吧

Runtime数据结构

还记得上文中提到的objc_msgSend:方法吧,来看看它的定义:

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

解释一下这个函数的使用:

  • 后面用了省略号,说明此函数参数不确定,可以传多个参数,使用objc_msgSend函数时可能会报错(Too many arguments to function call,expected 0,hava...),如下图是解决方案
    解决方案

    将Enable Strict Checking of objc_msgSend Calls 设置为 No
  • 当对象调用一个方法时,底层实际调用的就是这个函数(objc_msgSend),self代表方法调用者,
    [object test]; == objc_msgSend(object,@selector(test));
    初略介绍下objc_msgSend执行流程:


    执行流程

下面将会逐渐展开介绍一些术语,其实它们都对应着数据结构。

SEL

objc_msgSend函数第二个参数类型为SEL,在OC中它是用来表示@selector()类型的(Swift中是Selector类)。selector是方法选择器,可以理解为区分方法的 ID,而这个 ID 的数据结构是SEL:

typedef struct objc_selector *SEL;

其实它就是个映射到方法的C字符串,你可以用 Objc 编译器命令@selector()或者 Runtime 系统的sel_registerName函数来获得一个SEL类型的方法选择器。如果你知道selector对应的方法名是什么,可以通过NSString NSStringFromSelector(SEL aSelector)方法将SEL*转化为字符串,再用NSLog打印。

//返回给定选择器指定的方法的名称
const char * sel_getName ( SEL sel );

//在Objective-C Runtime系统中注册一个方法,将方法名映射到一个选择器,并返回这个选择器
SEL sel_registerName ( const char *str );

//在Objective-C Runtime系统中注册一个方法
SEL sel_getUid ( const char *str );

//比较两个选择器
BOOL sel_isEqual ( SEL lhs, SEL rhs );

上述函数使用案例:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        const char *name = [@"test" UTF8String];
        SEL sel = sel_registerName(name);
        NSLog(@"%s", sel_getName(sel));
        NSLog(@"%p", sel);
        
        SEL sel1 = @selector(test);
        NSLog(@"%s", sel_getName(sel1));
        NSLog(@"%p", sel1);
    }
    return 0;
}

控制台打印:

2016-04-14 16:53:22.446 Messaging[73956:10264811] test
2016-04-14 16:53:22.447 Messaging[73956:10264811] 0x7fff978441c8
2016-04-14 16:53:22.447 Messaging[73956:10264811] test
2016-04-14 16:53:22.447 Messaging[73956:10264811] 0x7fff978441c8

疑问:为什么sel的值和sel1的值相等?

因为不同类不管它们是父类与子类的关系,还是之间没有这种关系,它们相同名字的方法所对应的方法选择器(SEL)相同的(即共用同一个SEL),即使方法名相同而变量类型不同也会导致它们具有相同的方法选择器.正好证明上图中的@selector(test)是同一个SEL。

每一个方法都有一个对应SEL(也有可能多个方法对应同一个SEL)。编译器会根据每个方法的方法名为那个方法生成唯一的SEL。这些SEL组成了一个Set集合,当我们在这个集合中查找某个方法时,只需要去找这个方法对应的SEL即可。SEL实际上就是根据方法名hash化了的一个字符串,而对于字符串的比较仅仅需要比较他们的地址就可以了
如在某个类中定义以下两个方法:


test

这样的定义被认为是一种编译错误.
解释:

因为两个方法名相同,则@selector相同(共用一个@selector),不同类的实例对象执行相同的selector时,会在各自的方法列表中去根据selector去寻找自己对应的IMP(方法的实现)。当此(图中定义的类)类的一个实例调用setWidth:方法时,发现类中此方法有两个IMP(找一个方法的实现是根据方法名找的,跟方法的参数类型没关系),这样系统就不知道执行哪一个了,所以编译器禁止在一个类中定义两个同名的方法。所以在Objective-C同一个类(及类的继承体系)中,不能存在2个同名的方法,即使参数类型不同也不行。
这一点与Java不同,Java中可以定义同名,不同参(参数类型或者参数个数不同)的方法。

本质上,SEL只是一个指向方法的key(类似一个字典,根据key找value,在这里SEL相当于一个key,IMP相当于一个Value)(准确的说,SEL只是一个根据方法名hash化了的KEY值,能唯一代表一个方法),它的存在只是为了加快方法的查询速度。这个查找过程我们将在下面讨论。

我们可以在运行时添加新的selector,也可以在运行时获取已存在的selector,我们可以通过下面三种方法来获取SEL:

  • sel_registerName函数
    上文已介绍
  • Objective-C编译器提供的@selector()
    SEL sel = @selector(test);
  • NSSelectorFromString()方法
    SEL sel = NSSelectorFromString(@"test");

IMP

上文已提到IMP,现在就来说说IMP吧。
IMP实际上是一个函数指针(方法最终是以函数的形式调用),指向函数实现的首地址(通俗理解就是,IMP代表着.m文件中方法的实现)。这个被指向的方法包含一个接收消息的对象id,调用方法的SEL,以及一些方法参数,并返回一个id。其定义如下:

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

当你向某个对象发送一条信息,消息接收者(对象)通过SEL获得它所对应的IMP,在取得了函数指针之后,也就意味着我们取得了需要执行方法的代码入口,这样我们就可以像普通的C语言函数调用一样使用这个函数指针。

isa

Objective-C是一门面向对象的编程语言。每一个对象都是一个类的实例。其实在Objective-C中任何的类定义都是对象(类对象),在程序启动的时候任何类定义都对应于一块内存。在编译的时候,编译器会给每一个类生成一个且只生成一个”描述其定义的对象”,也就是苹果公司说的类对象(class object),他是一个单例(singleton), 而我们在C++等语言中所谓的对象,叫做实例对象(instance object)。对于实例对象我们不难理解,但类对象(class object)是干什么的呢?我们知道Objective-C是门动态语言,因此程序里的所有实例对象(instace object)都是在运行时由Objective-C的运行时库生成的,而这个类对象(class object)就是运行时库用来创建实例对象(instance object)的依据。
先来看看NSObject,Class,id的定义,再谈谈isa

@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}

typedef struct objc_class *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;

typedef struct objc_object *id;
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

从上述定义中可以看到运行时一个类关联了它的父类指针,类名,成员变量,方法,缓存,还有附属的协议。
每一个类对象描述了一系列它的实例的特点,包括成员变量的列表,成员函数的列表等。每一个对象都可以接受消息,而对象能够接收的消息列表是保存在它所对应的类中。
PS:OBJC2_UNAVAILABLE之类的宏定义是苹果在 Objc 中对系统运行版本进行约束的黑魔法,为的是兼容非Objective-C 2.0的遗留逻辑,但我们仍能从中获得一些有价值的信息,有兴趣的可以查看源代码。

在Objective-C语言的内部,每个对象都有一个名为isa的指针,指向该实例的类即类对象(类对象其实就是一个objc_class结构体)。既然类也是一个对象,那么类对象中应该也有个isa指针,那么类对象的isa指针指向什么呢?

类对象的isa指向的是它的元类对象(metaclass object),即类对象所属类的对象(关于metaclass下文会有解释)。

类对象与类实例的关系

类对象的实质:

类对象是由编译器创建的,即在编译时所谓的类,就是指类对象(官方文档中是这样说的: The class object is the compiled version of the class)。任何直接或间接继承了NSObject的类,它的实例对象(instance objec)中都有一个isa指针,指向这个实例对象的类对象(class object)。这个类对象(class object)中存储了关于这个实例对象(instace object)所属的类的定义的一切:包括变量,方法,遵守的协议等等。因此,类对象能访问所有关于这个类的信息,利用这些信息可以产生一个新的实例,但是类对象不能访问任何实例对象的内容(换一种方式理解就是:知道类有哪些属性,但不知道这些属性的值)。

区别

类对象保留了一个类实例的原型,但它并不是实例本身。它没有自己的实例变量,也不能执行那些类的实例的方法(只有实例对象才可以执行实例方法)。然而,类的定义能包含那些特意为类对象准备的方法–类方法( 而不是的实例方法)。类对象从父类那里继承类方法,就像实例从父类那里继承实例方法一样。

元类对象-metaclass object

What is a meta-class in Objective-C?,中文版->翻译 by Cocoabit;
元类对象的实质:

类对象是元类对象的一个实例!元类描述了 一个类对象,就像类对象描述了普通对象一样。不同的是元类的方法列表是类方法的集合(类对象的方法列表是它对应的实例对象的方法的集合),根据类对象的选择器来响应。当向一个类发送消息时,objc_msgSend会通过类对象的isa指针定位到元类,并检查元类的方法列表(包括父类)来决定调用哪个方法。元类代替了类对象描述了类方法,就像类对象代替了实例对象描述了实例化方法。
很显然,元类也是对象,也应该是其他类的实例,实际上元类是根元类(root class’s metaclass)的实例,而根元类是其自身的实例,即根元类的isa指针指向自身

这些理论可能有点绕,那就来张关系图(该图片来自这里) 说明一下吧,会更好理解:

关系图

Root Class 是指 NSObject

  • 每个Class都有一个isa指针指向一个唯一的Meta Class
  • 每一个Meta Class的isa指针都指向最上层的Meta Class(图中的Root class(NSObject)的Meta Class
    最上层的Meta Class的isa指针指向自己,形成一个回路
  • 每一个Meta Class的super class指针指向它原本Class的 Super Class的Meta Class。但是最上层的Meta Class的 Super Class指向NSObject Class本身
  • 最上层的Root class的super class指向 nil

当一个消息发送给任何一个对象, 方法的检查 从对象的 isa 指针指向的类开始(实例指向的是类对象),然后是父类。实例方法在类中定义, 类方法 在元类和根类中定义。(根类的元类就是根类自己)。在一些计算机语言的原理中,一个类和元类层次结构可以更自由的组成,更深元类链和从单一的元类继承的更多的实例化的类。
接下来看一下- (Class)class+ (Class)class方法的源码

// NSObject.mm
- (Class)class {
    return object_getClass(self);
}
+ (Class)class {
    return self;
}

// objc-class.mm
Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

实例对象调用- (Class)class返回的是它isa指向的对象,即类对象,而类对象调用+ (Class)class返回的是类对象自己,因为类对象是单例的,所以(以Person为例):

Person p = [[Person alloc] init];
BOOL flag = [p class] == [Person class] 

结果:flag = true

总结:

当一个类的实例调用- (Class)class方法,与这个类调用类方法+(Class)class返回的是同一个东西。

id

关于id的描述摘自:杨萧玉博客
objc_msgSend第一个参数类型为id,大家对它都不陌生,它是一个指向类实例的指针:

typedef struct objc_object *id;

那objc_object又是啥呢:

struct objc_object { Class isa; };

objc_object结构体包含一个isa指针,根据isa指针就可以顺藤摸瓜找到对象所属的类。
PS:isa指针不总是指向实例对象所属的类,不能依靠它来确定类型,而是应该用class方法来确定实例对象的类。因为KVO的实现机理就是将被观察对象的isa指针指向一个中间类而不是真实的类,这是一种叫做 isa-swizzling 的技术,详见官方文档

说了这么多,接下来就来说说objc_msgSend到底是怎么工作的吧

在 Objective-C 中,类、对象和方法都是一个 C 的结构体保存着
objc_msgSend(target,@selector(test))为例来说objc_msgSend的执行流程吧:

  • 1.检查selector是否需要忽略。(ps: Mac开发中开启GC就会忽略retain,release方法。)
  • 2.检查target是否为nil。如果为nil,直接cleanup,然后return。(这就是我们可以向nil发送消息的原因。)
  • 3.然后根据target的isa指针去它的Class中根据Selector去找IMP

寻找IMP的过程

  • 1.先从当前class的cache方法列表(cache methodLists)里去找
  • 2.找到了,跳到对应函数实现
  • 3.没找到,就从class的方法列表(methodLists)里找
  • 4.还找不到,就到super class的方法列表里找,直到找到基类(NSObject)为止
  • 5.最后再找不到,就会进入动态方法解析和消息转发的机制。(关于动态方法解析和转发机制,后面文章会有介绍)

再来个图文解说吧:


Objective-C方法调用流程:


图中的methodLists是什么呢?

上文当中已提到了objc_class结构体的定义,里面有个成员是

struct objc_method_list **methodLists;

根据名字和定义就可以猜到它是指向method的指针的集合(存的是指针)。
methodLists表示方法指针列表,它指向objc_method_list结构体的二级指针,可以动态修改*methodLists的值来添加成员方法,也是Category(关于Category会在后面的文章单独介绍)实现原理,同样也解释Category不能添加属性的原因。

再来看看objc_method_list的定义吧:

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;
}

objc_method_list

就是用来存储当前类的方法指针链表,objc_method存储了类的某个方法的信息。

Method

typedef struct objc_method *Method;

Method 是用来代表类中某个方法的类型,它实际就指向objc_method结构体,如下:

    struct objc_method {
    SEL method_name                     OBJC2_UNAVAILABLE;
    char *method_types                  OBJC2_UNAVAILABLE;
    IMP method_imp                      OBJC2_UNAVAILABLE;
}  
  • method_types是个char指针,存储着方法的参数类型和返回值类型。
  • SEL method_name 和 IMP method_imp 就是我们上文提到的,所以我们可以理解为objc_class中 method list保存了一组SEL<->IMP的映射

调用方法时,系统不可能每次这样去找IMP然后调用,这样效率太低了
怎么解决呢?

使用缓存将调用过的方法缓存起来,第二次调用的时候直接去缓存中找。

其实objc_class中cache就是用来干这个的。
那就来说说objc_class中的Cache
Cache
先来看看它的定义吧:

typedef struct objc_cache *Cache        OBJC2_UNAVAILABLE;  
  
struct objc_cache {  
/* total = mask + 1 */
unsigned int mask                   OBJC2_UNAVAILABLE;  
unsigned int occupied               OBJC2_UNAVAILABLE;  
Method buckets[1]                   OBJC2_UNAVAILABLE;  
};  
  • mask: 指定分配cache buckets的总数。在方法查找中,Runtime使用这个字段确定数组的索引位置
  • occupied: 实际占用cache buckets的总数
  • buckets: 指定Method数据结构指针的数组。这个数组可能包含不超过mask+1个元素。需要注意的是,指针可能是NULL,表示这个缓存bucket没有被占用,另外被占用的bucket可能是不连续的。这个数组可能会随着时间而增长。

objc_msgSend每调用一次方法后,就会把该方法缓存到cache列表中,下次调用的时候,就直接优先从cache列表中寻找,如果cache没有,才从methodLists中查找方法。
这样可以优化方法的调用性能,每当实例对象接收到一个消息时,它不会直接根据isa 指针指向的类的方法列表中遍历查找能够响应的方法,因为每次都要查找效率太低了,而是优先在 Cache 中查找。

Runtime 系统会把被调用的方法存到 Cache 中,如果一个方法被调用,那么它有可能今后还会被调用,下次查找的时候就会效率更高。就像计算机组成原理中 CPU 绕过主存先访问 Cache 一样。

如果想更深的理解cache,可参考:深入理解Objective-C:方法缓存 (write by 美团点评技术团队)
下一篇文章会详细介绍objc_msg...系列函数,消息转发动态解析method Swizzling,NSInvacation使用

如有不对的地方,请指正,谢谢!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容