runtime之objc_msgSend和消息转发
前言
OC是一门动态语言,就预示这光靠编译过程来完全读懂工作的方式是不够的,很多执行语句都需要在运行时才能知道其意思,也就是runtime。
理解OC中的消息传递
在Object-C中,函数的调用过程经常性的使用到方法,用来传递消息。而消息包括名称或选择子(selector),以及参数和返回值。
oc是一门动态语言,也就说明其消息可以是动态绑定的过程,在编译时,无法决定运行时该调用的函数。
例如该语句我们没有办法去知道返回值的类型:
id value = [Person sendMessage:@"黄"];
整个合起来就是一个消息,在其中,Person为接收者(receiver),sendMessage为选择子,后续为参数。当编译器收到该条消息是,通过C语言函数,也是消息传递机制中的核心函数objc_msgSend
进行消息传递。
void objc_msgSend(id self, SEL cmd, ...)
该函数能接受多个参数,其中第一个参数代表接收者,第二个参数是选择子,后跟可以跟多个发送消息参数。
objc_msgSend 消息传递机制
objc_msgSend
会先判断接收者对象是否存在,如果存在情况根据接收者所属的类中搜寻”方法列表“,如果找不到,则会向上找继承类的”方法列表“,找打找到合适就跳转,如果找不到则执行消息转发(后期会更新消息转发机制)。
该过程虽然繁琐,但是objc_msgSend
会为将每将匹配的结果缓存到”快速映射表“中,每个类都存在这样的缓存,后续若是需要再发送同样的消息,可以直接查找映射表,节省执行时间。
后续消息处理工作,会交给一些函数来处理:
objc_msgSend_stret
如果待发送的消息要返回结构体,可交由此函数处理。
objc_msgSend_fpret
如果待发送的消息要返回浮点数,可交由此函数处理。
objc_msgSendSuper
如果给父类发送消息,可交由此函数处理。
前面提到消息一旦找到就会跳转过去,之所以可以跳转,是因为OC对象的每个方法视为简单的C函数。
void Class_selector(id self, SEL cmd, ...)
每个类都有一张表,其中的指针都会指向这种函数,oc则以方法名作为key,来查表并执行跳转。
为了优化跳转方法变得简单一些,采用”尾调用优化“方式:如果某函数的最后一个操作是跳转到另一个函数,编译器会生成跳转所需的指令码,而不是向栈中新增新的帧,这样减少了,每次调用objc_msgSend
需要新增栈帧的问题,还有减少”栈溢出“的发生。但是仅在调用其他函数时,而不会把返回值另做他用的情况下。
根据上面的描述总结消息发送的步骤:
- 检查接收者对象是否为nil,是则直接结束转发,否则进入下一步;
- 查看类缓存的”快速映射表“中是否缓存该方法,该映射方法,是直接发送消息,否则进入下一步;
- 查看类的”方法列表“中是否存在,是缓存到映射表,并发送消息,否则进入下一步;
- 查看父类的”快速映射表“是否缓存改方法,有就发送,否则进入下一步;
- 查看父类的"方法列表"中是否存在,是缓存到映射表,并发送消息,否则回到第4步继续查找更上层的父类;
- 查找到没有父类之后,就说明并没有该方法,但是还不一定结束,后面会去进入
消息转发机制
的步骤。
动态方法解析
上述说到,当发送消息时,会出现无法解读消息的情况。即当消息发送到最后,依然找不到接收消息的方式时,oc对该消息可以进行更多处理,也就是动态方法解析
跟消息转发
。
oc是动态语言,在消息找不到目标时,编译器无法解析错误情况并不会报错,因为在运行时可以动态的给类添加方法,所以当无法解析消息是,就会启动消息转发
机制,可由程序员手动动态处理后续的消息问题。
如果程序员没有去处理这些问题,则会直接crach掉:
- (void)viewDidLoad {
[super viewDidLoad];
// 去调用一个没有实现的方法后
[self test];
}
// 删掉实现方法
//- (void)test { }
// 闪退并打印以下
*** -[ViewController test]: unrecognized selector sent to instance 0x100e01440
*** Terminating app due to uncaught exception 'NSInvalidArgumentException',
reason: '-[ViewController test]: unrecognized selector sent to instance 0x100e01440'
我们只调用test方法,但是没有调用实现方法,因此,如果没有做异常处理的话,会直接闪退。
消息转发两个阶段:
第一阶段是动态方法解析,先征询接收者所属的类,看是否能动态添加方法,以处理当>前”未知选择子(调用的方法)“。
第二阶段涉及完整的消息转发机制,若是第一阶段可以顺利完成,就不会启动消息转发。
显然如果能越早对消息进行处理,则减少运行时所耗费的时长。
动态方法解析有两个方法,包括实例方法解析和类方法解析
+ (BOOL)resolveClassMethod:(SEL)sel
+ (BOOL)resolveInstanceMethod:(SEL)sel
在该例子中,如所属类添加了resolveInstanceMethod
方法去处理,当调用的方法没有找到实现目标时,就会调用该实例方法去处理。
void dynamicMethodIMP(id self, SEL _cmd) { }
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 去调用一个没有实现的方法后
[self test];
}
// 删掉实现方法
//- (void)test { }
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(test)) {
class_addMethod([self class], sel, (IMP)dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
@end
如上图所示,不会异常闪退,因为当调用test方法时,找不到会直接调用resolveInstanceMethod
,而在resolveInstanceMethod
中对test,通过class_addMethod
动态添加。
备援接收者
如果resolveInstanceMethod
或者resolveClassMethod
返回YES就直接结束,如果返回NO,说明动态解析没有创建动态方法去处理,当前接收者还有第二次机会能处理未知选择子,在运行时,到这一步,就会问这条消息能不能转发给其他接收者处理。
- (id)forwardingTargetForSelector:(SEL)aSelector {
Person *person = [[Person alloc]init];
if (aSelector == @selector(test)) {
return person;
} else {
return [super forwardingTargetForSelector:aSelector];
}
}
当调用到这一步,如果返回对象,则是在告诉编译器,把这条消息传递给person对象来处理。到这里就不会出现异常,如果直接返回nil或者没有该对象,说明依然没办法处理结束并抛出异常。
注意:我们无法经由这一步操作消息转发,如果需要修改消息的内容,就要启用完整的消息转发
机制。
完整的消息转发
到这一步,说明只能启用消息转发,通过将有关消息的全部内容,创建NSInvocation
对象,并封装内容包括选择子、目标及参数。再触发NSInvocation
对象时,通过’消息派发系统‘将消息指派给目标对象。
- (void)forwardInvocation:(NSInvocation *)anInvocation
但是使用消息转发时必须为另一个类实现的消息创建一个有效的方法签名:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
实现的方式如下所致,其与第二步消息转发等效,但是却可以在消息触发前,改变消息的内容。比如:追加一个参数或者改变选择子等。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if(aSelector == @selector(test))
{
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return nil;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL selector = [anInvocation selector];
// 新建需要转发消息的对象
Cat *cat = [[Cat alloc] init];
if ([cat respondsToSelector:selector]) {
// 唤醒这个方法
[anInvocation invokeWithTarget:cat];
}
}
以上就是消息转发的几个步骤,每一步均有机会处理消息。步骤越往后,处理消息的代价就越大,如果可以在第一步就把消息处理完,运行时也可以将消息缓存起来。如果放到最后再做处理的话,不仅复杂还需要创建和处理完整的NSInvocation对象。