Objective-C runtime 消息传递与转发
栏目: Objective-C · 发布时间: 5年前
内容简介:Objective-C 本质上是一种基于 C 语言的领域特定语言。Objective-C 通过一个用 C 语言和汇编实现的 runtime,在 C 语言的基础上实现了面向对象的功能。在 runtime 中,对象用结构体表示,方法用函数表示。C 语言是一门静态语言,其在编译时决定调用哪个函数。而 Objective-C 则是一门动态语言,其在编译时不能决定最终执行时调用哪个函数(Objective-C 中函数调用称为消息传递)。Objective-C 的这种动态绑定机制正是通过 runtime 这样一个中间
Objective-C 本质上是一种基于 C 语言的领域特定语言。Objective-C 通过一个用 C 语言和汇编实现的 runtime,在 C 语言的基础上实现了面向对象的功能。在 runtime 中,对象用结构体表示,方法用函数表示。
C 语言是一门静态语言,其在编译时决定调用哪个函数。而 Objective-C 则是一门动态语言,其在编译时不能决定最终执行时调用哪个函数(Objective-C 中函数调用称为消息传递)。Objective-C 的这种动态绑定机制正是通过 runtime 这样一个中间层实现的。
为了分析 runtime 是如何进行动态绑定,我们首先需要了解一下 Objective-C 中类与对象等基本结构在 C 语言层面是如何实现的。
数据结构
Objective-C 类
Objective-C 类是由 Class
类型表示的,它本质上是一个指向 objc_class
结构体的指针。如下所示为 objc/runtime.h
中关于类的定义:
typedef struct object_class *Class struct object_class{ Class isa OBJC_ISA_AVAILABILITY; #if !__OBJC2__ Class super_class OBJC2_UNAVAILABLE; // 父类 const char *name OBJC2_UNAVAILABLE; // 类名 long version OBJC2_UNAVAILABLE; // 类的版本信息,默认为0 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 *` */
Objective-C 对象
Objective-C 对象是由 id
类型表示的,它本质上是一个指向 objc_object
结构体的指针。如下所示为 objc/objc.h
中关于对象的定义:
typedef struct objc_object *id; struct objc_object{ Class isa OBJC_ISA_AVAILABILITY; };
objc_object
结构体中只有一个成员,即指向其类的 isa
指针。 当向一个 Objective-C 对象发送消息时,runtime 会根据实例对象的 isa
指针找到其所属的类。Runtime 会在类的方法列表以及父类的方法列表中去寻找与消息对应的 selector
指向的方法,找到后即运行该方法。
Objective-C 元类(meta class)
meta-class
是一个 类对象的类 。在 Objective-C 中,所有的类本身也是一个对象。事实上,在很多原型编程语言也采用这种“万物皆对象”的设计思想,如:Io。
通过向该对象发送消息,即可实现对类方法的调用。前提是类的 isa
指针必须指向一个包含这些类方法的 objc_class
结构体。 meta-class
中存储着一个类的所有类方法。所以,类对象的 isa
指针指向的就是 meta-class
。
- 当向一个对象发送消息时,runtime 会在这个对象所属的类的方法列表中查找方法。
- 当向一个类发送消息时,runtime 会在这个类的 meta-class 的方法列表中查找。
思考一下, meta-class
也是一个类,也可以向它发送一个消息,那么它的 isa
又是指向什么呢?为了不让这种结构无限延伸下去,Objective-C 的设计者让所有的 meta-class
的 isa
指向基类的 meta-class
,以此作为它们的所属类。
下图所示,为 Objective-C 对象在内存中的引用关系图。
Objective-C 方法
方法实际上是一个指向 objc_method
结构体的指针。如下所示为 objc/runtime.h
中关于方法的定义:
typedef struct objc_method *Method struct objc_method{ SEL method_name OBJC2_UNAVAILABLE; // 方法名 char *method_types OBJC2_UNAVAILABLE; IMP method_imp OBJC2_UNAVAILABLE; // 方法实现 }
结构体中包含成员 SEL
和 IMP
,两者将方法的名字与实现进行了绑定。通过 SEL
,可以找到对应的 IMP
,从而调用方法的具体实现。
SEL
SEL
又称选择器,是一个指向 objc_selector
结构体的指针。
typedef struct objc_selector *SEL;
方法的 selector
用于表示运行时方法的名字。 Objective-C 在编译时,会根据方法的名字(不包括参数)生成一个唯一的整型标识( Int
类型的地址),即 SEL
。
由于一个类的方法列表中不能存在两个相同的 SEL
,所以 Objective-C 不支持重载。但是不同类之间可以存在相同的 SEL
,因为不同类的实例对象执行相同的 selector
时,会在各自的方法列表中去根据 selector
去寻找自己对应的 IMP
。
通过下面三种方法可以获取 SEL
:
sel_registerName @selector() NSSeletorFromString()
IMP
IMP
本质上就是一个函数指针,指向方法实现的地址。
typedef id (*IMP)(id, SEL,...);
参数说明
-
id
:指向self
的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针) -
SEL
:方法选择器 -
...
:方法的参数列表
SEL
与 IMP
的关系类似于哈希表中 key
与 value
的关系。采用这种哈希映射的方式可以加快方法的查找速度。
消息传递(方法调用)
在 Objective-C 中, 消息直到运行时才绑定到方法实现上 。编译器会将消息表达式 [receiver message]
转化为一个消息函数的调用,即 objc_msgSend
。这个函数将消息接收者和方法名作为主要参数,如下所示:
objc_msgSend(receiver, selector) // 不带参数 objc_msgSend(receiver, selector, arg1, arg2,...) // 带参数
objc_msgSend
通过以下几个步骤实现了动态绑定机制。
- 首先,获取
selector
指向的方法实现。由于相同的方法可能在不同的类中有着不同的实现,因此根据receiver
所属的类进行判断。 - 其次,传递
receiver
对象、方法指定的参数来调用方法实现。 - 最后,返回方法实现的返回值。
消息传递的关键在于前文讨论过的 objc_class
结构体,其有两个关键的字段:
-
isa
:指向父类的指针 -
methodLists
: 类的方法分发表(dispatch table
)
当创建一个新对象时,先为其分配内存,并初始化其成员变量。其中 isa
指针也会被初始化,让对象可以访问类及类的继承链。
下图所示为消息传递过程的示意图。
- 当消息传递给一个对象时,首先从运行时系统缓存
objc_cache
中进行查找。如果找到,则执行。否则,继续执行下面步骤。 -
objc_msgSend
通过对象的isa
指针获取到类的结构体,然后在方法分发表methodLists
中查找方法的selector
。如果未找到,将沿着类的isa
找到其父类,并在父类的分发表methodLists
中继续查找。 - 以此类推,一直沿着类的继承链追溯至
NSObject
类。一旦找到selector
,传入相应的参数来执行方法的具体实现,并将该方法加入缓存objc_cache
。如果最后仍然没有找到selector
,则会进入 消息转发 流程(下文将进行介绍)。
消息转发
当一个对象能接收一个消息时,会走正常的消息传递流程。当一个对象无法接收某一消息时,会发生什么呢?默认情况下,如果以 [object message]
的形式调用方法,如果 object
无法响应 message
消息时,编译器会报错。如果是以 performSeletor:
的形式调用方法,则需要等到运行时才能确定 object
是否能接收 message
消息。如果不能,则程序崩溃。
对于后者,当不确定一个对象是否能接收某个消息时,可以调用 respondsToSelector:
来进行判断。
if ([self respondsToSelector:@selector(method)]) { [self performSelector:@selector(method)]; }
事实上,当一个对象无法接收某一消息时,就会启动所谓“消息转发(message forwarding)”机制。通过消息转发机制,我们可以告诉对象如何处理未知的消息。
消息转发机制大致可分为三个步骤:
- 动态方法解析(Dynamic Method Resolution)
- 备用接收者
- 完整消息转发
下图所示为消息转发过程的示意图。
动态消息解析
对象在接收到未知的消息时,首先会调用所属类的类方法 +resolveClassMethod:
或实例方法 +resolveInstanceMethod:
。
在这两个方法中,我们可以为未知消息新增一个“处理方法”,通过运行时 class_addMethod
函数动态添加到类中。比如:
void dynamicMethodIMP(id self, SEL _cmd) { // 方法实现 } + (BOOL)resolveInstanceMethod:(SEL)aSEL { if (aSEL == @selector(resolveThisMethodDynamically)) { class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:"); return YES; } return [super resolveInstanceMethod:aSEL]; }
这种方案更多的是为了实现@dynamic属性。
备用接收者
如果在上一步无法处理消息,则 runtime 会继续调用 - (id)forwardingTargetForSelector:(SEL)aSelector
方法。
如果一个对象实现了这个方法,并返回一个非 nil
(也不能是 self
) 的对象,则这个对象会称为消息的新接收者,消息会被分发到这个对象。比如:
- (id)forwardingTargetForSelector:(SEL)aSelector { NSString * selString = NSStringFromSelector(aSelector); if ([selString isEqualToString:@"walk"]) { return self.otherObject; } return [super forwardingTargetForSelector:aSelector]; }
这一步合适于我们只想将消息转发到另一个能处理该消息的对象上。但这一步无法对消息进行处理,如操作消息的参数和返回值。
完整消息转发
如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。
这步调用 methodSignatureForSelector
进行方法签名,这可以将函数的参数类型和返回值封装。如果返回 nil
,则说明消息无法处理并报错 unrecognized selector sent to instance
。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { if ([NSStringFromSelector(aSelector) isEqualToString:@"testInstanceMethod"]){ return [NSMethodSignature signatureWithObjcTypes:"v@:"]; } return [super methodSignatureForSelector: aSelector]; }
如果返回 methodSignature
,则进入 forwardInvocation
。对象会创建一个表示消息的 NSInvocation
对象,把与尚未处理的消息有关的全部细节都封装在 anInvocation
中,包括 selector
, target
,参数。在这个方法中可以修改实现方法,修改响应对象等,如果方法调用成功,则结束。如果依然不能正确响应消息,则报错 unrecognized selector sent to instance
。
- (void)forwardInvovation:(NSInvocation)anInvocation { [anInvocation invokeWithTarget:_helper]; [anInvocation setSelector:@selector(run)]; [anInvocation invokeWithTarget:self]; }
可以利用备用接受者和完整消息转发实现对接受消息对象的转移,可以实现“多重继承”的效果。
参考
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。