iOS开发之消息转发与三次拯救

2020-04-27  本文已影响0人  萨缪

一、消息概述

1、消息发送机制

在OC中,方法的调用不再理解为对象调用其方法,而是要理解成对象接收消息,消息的发送采用"动态绑定"机制,具体会调用哪个方法直到运行时才能确定,确定后才会去执行绑定的代码。

方法的调用实际上就是告诉对象要干什么,给对象传递一个消息,对象为接收者(receiver),调用的方法及参数就是消息(message),给一个对象传递消息表达为[receiver message];接收者的类型可以通过动态类型识别在运行时确定。

在消息传递机制中,当开发者编写[receiver message];语句发送消息后,编译器都会将其转化成对应的一条objc_msgSend C语言消息发送原语。具体格式为: void objc_msgSend(id self,SEL cmd,....)

这个原语函数参数可变,第一个参数填入消息的接收者,第二个参数就是消息"选择子",后面跟着可选的消息的参数。有了这些参数,objc_msgSend就可以通过接收者的isa指针,到其类对象的方法列表中以选择子的名称为"键"寻找对应的方法。若找到对应的方法,则转到其实现代码执行,否则继续从父类中寻找,如果到根类还是无法找到对应的方法,说明该接收者对象响应该消息,那么就会触发消息转发机制,给开发者最后一次挽救程序crash的机会。

2、消息转化机制

在Objective-C中,使用对象进行方法调用是一个消息发送的过程(Objective-C采用“动态绑定机制”,所以所要调用的方法直到运行期才能确定)。

方法在调用时,系统会查看这个对象能否接收这个消息(查看这个类有没有这个方法,或者有没有实现这个方法。),如果不能并且只在不能的情况下,就会调用下面这几个方法,给你“补救”的机会,你可以先理解为几套防止程序crash的备选方案,苹果就是利用这几个方案进行消息转发,注意一点,前一套方案实现后一套方法就不会执行。如果这几套方案你都没有做处理,那么程序就会报错crash。

3.消息转发的三道防线

如果在消息传递过程中,接收者无法响应收到的信息,那么就会触发进入消息转发机制。

当一个对象无法接收某一消息时,就会启动所谓”消息转发(message forwarding)“机制,通过这一机制,我们可以告诉对象如何处理未知的消息。默认情况下,对象接收到未知的消息,会导致程序崩溃,通过控制台,我们可以看到以下异常信息:

-[SUTRuntimeMethod method]: unrecognized selector sent to instance 0x100111940

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[SUTRuntimeMethod method]: unrecognized selector sent to instance 0x100111940'

这段异常信息实际上是由NSObject的”doesNotRecognizeSelector”方法抛出的。不过,我们可以采取一些措施,让我们的程序执行特定的逻辑,而避免程序的崩溃。

消息转发机制基本上分为三个步骤:

动态方法解析

备用接收者

完整转发

这也可以叫作消息转发提供的3道防线,任何一个起作用都可以挽救此次消息转发。

按照先后顺序3道防线依次为

1)动态补加方法实现

(BOOL)resolveInstanceMethod:(SEL)sel

(BOOL)resolveClassMethod:(SEL)sel

(2)直接返回消息转发到的对象(将消息发送给另一对象去处理)

(id)forwardingTargetForSelector:(SEL)aSelector

(3)手动生成方法签名并转发给另一对象

(NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector

(void)forwardInvocation:(NSInvocation *)anInvocation

上图显示了消息转发的具体流程,接收者在每一步中均有机会处理消息。步骤越往后处理消息的代价越大。首先,会调用

(BOOL)resolveInstanceMethod:(SEL)sel。

若方法返回YES,则表示可以处理该消息。在这个过程,可以动态地给消息增加方法。

// Person.m

// 不自动生成getter和setter方法

@dynamic name;

+ (BOOL)resolveInstanceMethod:(SEL)sel

{

if (sel == @selector(name)) {

// BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)

//@@: 此为签名符号

class_addMethod(self, sel, (IMP)GetterName, "@@:");

return YES;

}

if (sel == @selector(setName:)) {

class_addMethod(self, sel, (IMP)SetterName, "v@:@");

return YES;

}

return [super resolveInstanceMethod:sel];

}

// (用于类方法)

//+ (BOOL)resolveClassMethod:(SEL)sel

//{

//    NSLog(@"resolveClassMethod called %@", NSStringFromSelector(sel));

//

//    return [super resolveClassMethod:sel];

//}

id GetterName(id self, SEL cmd)

{

NSLog(@"%@, %s", [self class], sel_getName(cmd));

return @"Getter called";

}

void SetterName(id self, SEL cmd, NSString *value)

{

NSLog(@"%@, %s, %@", [self class], sel_getName(cmd), value);

NSLog(@"SetterName called"

);

签名符号含义:

举个例子:- (void)printStr1:(NSString *)str 对应的ObjCTypes 为 v@:@。

'v‘ : void类型,第一个字符代表返回值类型

’@‘ : 一个id类型的对象,第一个参数类型

’:‘ : 对应SEL,第二个参数类型

’@‘ : 一个id类型的对象,第三个参数类型,也就是- (void)printStr1:(NSString*)str中的str。

printStr1:本来是一个参数,ObjCTypes怎么成了三个参数?要理解这个还必须理解OC中的消息机制。一个method对应的结构体如下,ObjCTypes中的参数其实与IMP

method_imp 函数指针指向的函数的参数相一致。相关内容有很多,不了解的可以参考这篇文章方法与消息。

typedef struct objc_method *Method;

struct objc_method {

SEL method_name                OBJC2_UNAVAILABLE;  // 方法名

char *method_types                  OBJC2_UNAVAILABLE;

IMP method_imp                      OBJC2_UNAVAILABLE;  // 方法实现

}

*          代表  char *

char BOOL  代表  c

:          代表  SEL

^type      代表  type *

@          代表  NSObject * 或 id

^@        代表  NSError **

#          代表  NSObject

v          代表  void

// main.m

/* 现在在main.m中给Person发送setName:和name消息,由于Person中未实现这两个方法,就会经消息转发调用GetterName和SetterName方法

*/

Person *person = [[Person alloc] init];

[person setName:@"Jake"];

NSLog(@"%@", [person name]);

// 输出结果:

Person, setName:, Jake

SetterName called

Person, name

Getter called

若方法返回NO,则进行消息转发的第二步,查找是否有其它的接收者。对应的处理函数是:

- (id)forwardingTargetForSelector:(SEL)aSelector。

可以通过该函数返回一个可以处理该消息的对象。

现在新建一个类Child,在Child中实现一个eat方法,在Person类中定义eat方法但不实现它。

// Child.m

- (void)eat

{

NSLog(@"Child method eat called");

}

然后在Person类中实现forwardingTargetForSelector:方法:

// Person.m

// 当调用Person中的eat方法时,由于Person中并未实现该方法,就会经下面的方法将消息转发给可以处理eat方法的对象

- (id)forwardingTargetForSelector:(SEL)aSelector

{

NSString *selStr = NSStringFromSelector(aSelector);

if ([selStr isEqualToString:@"eat"]) {

return [[Child alloc] init];        // 这里返回Child类对象,让Child去处理eat消息

}

return [super forwardingTargetForSelector:aSelector];

}

// main.m

[person eat];

// 输出结果:

Child method eat called

通过此方案,我们可以用“组合”来模拟出“多重继承”的某些特性。在一个对象内部,可能还有一系列其他对象,该对象可以经由此方法将能够处理某选择子的相关内部对象返回,这样的话,在外界看来好像是该对象亲自处理了这些消息。

伪多继承与真正的多继承的区别在于,真正的多继承是将多个类的功能组合到一个对象中,而消息转发实现的伪多继承,对应的功能仍然分布在多个对象中,但是将多个对象的区别对消息发送者透明。

若第二步返回nil,则进入消息转发的第三步。调用

- (void)forwardInvocation:(NSInvocation )anInvocation。

运行时系统会在这一步给消息接收者最后一次机会将消息转发给其它对象。对象会创建一个表示消息的NSInvocation对象,把与尚未处理的消息 有关的全部细节都封装在anInvocation中,包括selector,目标(target)和参数。我们可以在forwardInvocation 方法中选择将消息转发给其它对象。

这个方法实现得很简单。只需要改变调用目标,使消息在新目标上得以调用即可。不过,如果采用这种方式,实现的效果与第二步的消息转发是一致的。所以比较有用的实现方式是:先以某种方式改变消息内容,比如追加另外一个参数,或者改换选择子,等等。

// Person.m

//我们必须重写该方法 消息转发机制使用从这个方法中获取的信息来创建NSInvocation对象。因此我们必须重写这个方法,为给定的selector提供一个合适的方法签名。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

{

NSString *sel = NSStringFromSelector(aSelector);

// 判断要转发的SEL

if ([sel isEqualToString:@"sleep"]) {

// 为转发的方法手动生成签名

return [NSMethodSignature signatureWithObjCTypes:"v@:"];

//那么NSMethodSignature又是什么?来看看

}

return [super methodSignatureForSelector:aSelector];

}

//NSObject的forwardInvocation:方法实现只是简单调用了doesNotRecognizeSelector:方法,它不会转发任何消息。这样,如果不在以上所述的三个步骤中处理未知消息,则会引发一个异常。

//转发消息

- (void)forwardInvocation:(NSInvocation *)anInvocation

{

//拿到消息

SEL selector = [anInvocation selector];

// 新建需要转发消息的对象 转发消息

Child *child = [[Child alloc] init];

if ([child respondsToSelector:selector]) {

// 转发 唤醒这个方法

[anInvocation invokeWithTarget:child];

} else {

[super forwardInvocation:anInvocation];

}

}

//从某种意义上来讲,forwardInvocation:就像一个未知消息的分发中心,将这些未知的消息转发给其它对象。或者也可以像一个运输站一样将所有未知消息都发送给同一个接收对象。这取决于具体的实现。

// Child.h

#import

@interface Child : NSObject

- (void)eat;

- (void)sleep;

@end

// Child.m

- (void)sleep

{

NSLog(@"Child method sleep called");

}

// 输出结果:

Child method sleep called

上面这个例子就是在Child中写sleep方法 并使用标准消息转发的方式来预防

当Child的子类想要调用sleep方法但却没有实现时 就可以通过标准消息转发的方式来查找父类方法列表中是否有该方法。

有时候服务器很烦不靠谱,老是不经意间返回null,可以重写NSNull的消息转发方法, 让他能处理这些异常的方法,达到解决问题的目的。

上一篇下一篇

猜你喜欢

热点阅读