说到 Objective-C Runtime ,可能不是大家常常提及的内容。但它确实又和大家平时的开发过程息息相关,即使使用 Swift 语言,也依然离不开 Objective-C Runtime。 咱们就来一探究竟吧。
什么是 Objective-C Runtime
使用过 Objective-C 进行开发的同学一定会注意到 Objective-C 中的 Selector 机制。 为什么要把它称为 Selector 呢,它和函数和方法有什么区别呢? 比如给 UIButton 添加事件的时候:
[button addTarget:self action:@selector(buttonClciked) forControlEvents:UIControlEventTouchUpInside];
为什么给 action 参数传递进来的是一个 selector 而不是一个函数的名称呢?
这就要从 Objective-C Runtime 说起。 所有的 Objective-C 方法调用都是基于 Objective-C Runtime 进行的。 比如最简单的方法调用:
[person sayHello];
如果按照面向对象的思维去解释,可以将这行代码解释为调用 person
对象的 sayHello
方法。 但如果从 Objective-C Runtime 的角度来说,这个代码实际上是在发送一个消息。
要牢牢记住上面的代码是发送消息。 刚刚那段代码,编译器实际上会将它转换成这样一个函数调用:
objc_msgSend(person,sayHello)
objc_msgSend
是 Objective-C Runtime 中的函数,这个函数定义在
头文件中。
我们在 Objective-C 中所有通过一对方括号所进行的方法调用,其实都是通过 Objective-C Runtime 的 objc_msgSend
函数发送的一个消息传递。
objc_msgSend
那么既然所有的方法调用本质上都是通过 objc_msgSend
进行的消息传递。 那么 objc_msgSend
这个函数做了什么呢?
objc_msgSend
负责 Objective-C Runtime 中消息机制的核心 - 叫做消息分发。
在了解消息分发之前,咱们还需要了解 runtime 中关于类的定义,
头文件中定义了这样一个结构:
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
}
大家可能会想了, 怎么又冒出这么一个 struct 结构呢? 我们不是已经在 Objective-C 用诸如这样的代码定义过我们的类了么:
@interface Person : NSObject
- (void) sayHello;
@end
@implementation Person
- (void)sayHello {
NSLog(@"hello");
}
@end
简单来说呢,咱们用 Objective-C 代码定义了类,比如我们这里定义的 Person
类,都是应用层的逻辑。它服务于我们开发的 App 的需求 - 比如,有这样一个 Person 类,它可以通过 sayHello 方法向命令行输出内容。
但在系统层级,我们定义的 Person 类是如何在内存中表示的?
对 Person 类中方法的调用是如何实现的呢?
这些系统层级的逻辑就要靠 Objective-C Runtime 为我们完成了。
比如 Person 类中所定义的属性和方法,在内存中的存储方式就是通过 Runtime 的 struct objc_class
结构来定义了。每一个类的实例在 Runtime 中都会用 objc_class
这个结构来表示,这也就意味着所有的对象也都包含了 objc_class 结构中所定义的属性。
那么我们继续, objc_class
结构包含了很多属性, 其中一个叫做 isa
, 它的类型是 Class
。 那么继续追根溯源,在
中找到了 Class
类型的定义:
typedef struct objc_class *Class;
实际上 isa
的类型,就是 objc_class
这个结构的类型。 isa
所指向的结构正是这个类的元信息(属性,方法的定义)。
现在,我们对 Runtime 的基础结构有了一个了解。 再回到 objc_msgSend
函数中, 它的第一个参数就是我们要发送消息的实例。首先,objc_msgSend
函数会检测这个实例的 isa
属性,找到 isa
中定义的:
struct objc_method_list **methodLists
methodLists 属性表示当前实例的方法列表,它是一个 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;
}
这个结构的定义可能大家会有些地方不太明白,比如 space
属性是干什么的。咱们可以暂时抛开这些问题,只关心和消息分发相关的属性 - method_count
属性表示当前这个实例中方法的个数,method_list
结构表示当前实例上面所有方法的列表:
struct objc_method method_list[1]
它的每一个元素又是一个 objc_method
类型的结构:
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
这个结构有三个属性,method_name 是 SEL 类型。 就是 @selector(sayHello)
这样的表达式所表示的类型。 也就是我们所说的 Selector
。 看到这里是不是有些豁然开朗的感觉呢?
Selector ,它其实是 Runtime 的一个数据结构。它代表一个方法的唯一标识。
然后再看第二个属性 method_types
, 这个属性用一个字符串表示方法返回值类型以及每个参数的类型。 它使用 @encode
规则对类型信息进行编码,咱们现在只要了解到这就好,细节先不深究。
最后一个参数是 IMP 类型,它表示这个 Selector 对应的函数的地址。 对,是函数没错。 Objective-C 中定义的所有类的方法在底层实现上就是一个函数。
消息分发流程
咱们对 Objective-C Runtime 的消息的底层数据结构已经有了足够的了解。 接下来就探讨一下消息分发机制吧。
说了这么多之后大家还记得是怎么调用 objc_msgSend
函数的么? 咱们再来回顾一下:
objc_msgSend(person,@selector(sayHello))
第一个参数是要发送消息的实例,也就是 person 对象。 objc_msgSend
会先查询它的 methodLists
方法列表,使用第二个参数 sayHello
逐个和 person
的 methodLists
中的每一个方法信息的 SEL 进行对比,如果找到对应的方法,就调用它所对应的函数,也就是 IMP,然后调用这个函数。
消息分发的基本流程用一张图来描绘:
这张流程图最后一步,我们看到这样调用 sayHello 函数:
sayHello(person,@selector(sayHello));
是不是觉得有点奇怪? sayHello 方法我们明明是这样定义的:
- (void) sayHello;
它不接受任何参数,而我们流程图中的 sayHello 却传入了两个参数。这就引出了 runtime 的另外一个机制,我们继续讨论。
方法实现
Objective-C 中所有的方法调用,其实都会隐式的传递进来两个参数。第一个参数我们比较熟悉了, 就是 self
。 只不过我们习以为常的把 self 当成一个关键字,其实它是一个传递进来的参数。
第二个参数叫做 _cmd
用于表示当前函数所对应的 Selector。 这个参数很少会用到,咱们不进一步展开。
这就解释了我们刚才的问题, Objective-C 中即便这个方法声明为不接受任何参数,但在实际调用它的时候,也会至少将这两个隐含的参数传递进来。 这个两个参数的传递过程,我们在应用层开发的时候是完全不用管的,这些工作都由 Objective-C Runtime 替我们完成了。
更进一步的说,我们为 Person 定义的 sayHello 方法,在 runtime 中实际上就是一个函数而已。 它的签名如下:
void sayHello(Person person, SEL _cmd) { ... }
有了这个概念后,我们就更加理解 objc_msgSend
消息分发的过程了。 Objective-C Runtime 用 objc_msgSend(person,@selector(sayHello))
这样的方式将 sayHello 消息发送给 person 实例。 objc_msgSend
函数找到 @selector(sayHello) 在 Person 类中所对应的函数,然后调用这个函数,并传入两个隐含的参数。
从这个流程不难看出, Selector 实际上是函数的一个标识,它不是函数。
runtime 通过 SEL 类型的 Selector 标识,在 Person 类的方法列表中找到和这个 Selector 相同的条目, 然后执行这个条目 IMP 属性所指向的函数地址,并传入 self 和 _cmd 两个隐含的参数。
回想一下前面提到的 objc_method 结构的定义:
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
这就更加明确了, SEL 类型的 method_name
属性仅仅作为一个标识。 而 method_imp
才是真正要执行的函数地址。
消息缓存
经过了前面长篇大论的分析,了解了 Objective-C Runtime 的消息分发的整体流程,这样一个简单的方法调用:
[person sayHello];
实际上在它的背后,对应着一系列复杂的机制。实际上在 Objective-C 中调用一个方法需要两个过程,首先通过消息分发找到对应的函数
注意这里是函数,在 runtime 中只有函数(Function)
然后再调用这个函数,并传递相应的参数。
实际上消息分发的过程是比较消耗性能的,需要进行一系列的查表操作。 所以 Objective-C Runtime 对消息的分发建立了缓存机制。 这点我们可以回顾一下 objc_class
结构的定义,是否还记得它也定义了一个 cache
属性:
struct objc_class {
...
struct objc_cache *cache OBJC2_UNAVAILABLE;
...
}
它的类型是 objc_cache
, 继续找到这个结构的定义:
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};
实际上 objc_cache
维护了一个哈希表,使用 Selector 作为键,存储了缓存的函数列表。
了解了这些,消息分发的时候,首先会在 cache
里面进行匹配,如果我们发送的消息所对应的函数在 cache 中能够找到,就直接执行这个函数了。 如果 cache 中没有,才会真的去查找 methodLists
列表,并且成功匹配一次后,就将它放入缓存中,以后再调用这个方法就不会重新的进行查表操作了。
消息分发的传递机制
我们在发送一个消息时,如果当前实例的方法列表中没找到对应的函数怎么办呢,比如我们发送这样一个消息:
[person description]
Person 类中确实没有定义 description
方法。对于这样的情况, Objective-C Runtime 会继续查找它的父类,使用定义在 objc_class
结构中的 super_class
属性。
struct objc_class {
...
Class super_class OBJC2_UNAVAILABLE;
...
}
直到查找到最顶层的根类。 比如在我们上面的例子中, Person 类确实没有定义 description 方法,但它的父类 NSObject
是有这个方法的,所以就会执行定义在 NSObject
的方法列表中的这个函数。 这个流程也很好的解释了 Objective-C 中方法重载的机制。 Runtime 会现在子类的 methodLists 中查找,如果子类有相应的重载,就会优先使用子类的实现。
如果遍历完整个类层级依然找不到对应的方法实现,默认情况下就会抛出类似这样的异常:
unrecognized selector sent to instance 0x7fe672452350
相信大家在开发中会不少次遇到这种情况吧。 不过,这只是默认行为,其实这个异常是可以不抛出的。 我们完全可以在发送了一个并没有实现的消息的时候不让程序崩溃。 这就涉及到 runtime 的消息转发机制了。 这次就先不讨论啦,改天帮大家总结一篇单独的文章。
直接发送消息
介绍了这么多,相信大家通过这篇文章的内容,对 Runtime 的消息机制已经有了比较多的了解。 所有的方法调用,在 Runtime 中都会通过 objc_msgSend
来发送。 如果这么说来其实是可以直接调用 objc_msgSend
来发送消息的。 可以验证一下:
#import
。。。
((void (*)(id, SEL)) objc_msgSend)(person,@selector(sayHello));
这段代码是可以在真实环境中编译并运行的。 运行程序后,控制台上会有这样的输出:
hello
这说明我们通过直接调用 objc_msgSend
的方式完成了方法调用。 解释一下刚才的代码, 首先需要引入 Runtime 的头文件 #import
。
message.h 中定义的 objc_msgSend 函数,并没有明确参数列表和返回类型, 所以我们需要强制转换一下,否则我们会遇到编译错误:
((void (*)(id, SEL)) objc_msgSend)
然后调用这个转换后的函数,并传入相应的参数:
((void (*)(id, SEL)) objc_msgSend)(person,@selector(sayHello));
这样,编译顺利通过,验证成功~
直接调用函数
虽然我们可以调用 objc_msgSend
来发送消息,但它还是要经过消息分发的过程。 当然,如果你需要的话,你是可以完全绕过消息分发机制直接调用函数的。 NSObject
中定义了一个 methodForSelector
方法,可以得到 Selector 所对应的函数:
void (*sayHello)(id, SEL);
sayHello = (void (*)(id,SEL))[person methodForSelector:@selector(sayHello)];
sayHello(person, @selector(sayHello));
我们通过 methodForSelector
得到了 sayHello
函数的地址引用,这样我们就可以直接调用 sayHello
函数了,这样就会绕过 Runtime 的消息分发机制。
当然,这两个小例子主要是帮助大家了解 Objective-C Runtime 的机制。我们在实际代码中,如非必要,还是不建议绕过默认的消息分发机制。
总结
Objective-C Runtime 可以说是隐藏在幕后的精英。 我们所写的几乎每一行代码,都和 Objective-C Runtime 形影不离。 比如,传递给 objc_msgSend
的第一个参数是 nil
的话, objc_msgSend
就会判断并进行短路操作。 这也解释了为什么在值为 nil 的引用上面调用方法不会导致程序崩溃。 Objective-C Runtime 中所涉及的内容,以及它定义的函数,很少会再我们的日常开发中用到。但了解 Objective-C Runtime 背后的机制,会让你在写代码的时候更加有把握,有一种内功暴增的感觉。
更多精彩内容可关注微信公众号:
swift-cafe