第十二条 消息转发机制(Message forwarding)

2017/8/22 posted in  第二章 对象,消息,运行期

第11条讲解了对象的消息传递机制,并强调了其重要性。第12条则要讲解另外一个重要的问题,就是对象在收到无法解读的消息之后会发生什么情况。若想令类能理解某条消息,我们必须以程序码实现出对应的方法才行。但是,在编译期向类发送了其无法解读的消息并不会报错,因为在运行期可以继续向类中添加方法,所以编译器在编译时还无法确知类中到底会不会有某个方法实现。当对象接收到无法解读的消息后,就会启动“消息转发"(message forwarding)机制,程序员可经由此过程告沂对象应该如何处理未知消息。

消息转发分为两大阶段:

  1. 先征询接收者,所属的类,看其是否能动态添加方法,以处理当前这个“未知的选择子"(unknown selector),这叫做“动态方法解析”(dynamic method resolution)。

  2. 涉及“完整的消息转发机制”(full forwarding mechanism)。如果运行期系统已经把第一阶段执行完了,那么接收者自己就无法再以动态新增方法的手段来响应包含该选择子的消息了。此时,运行期系统会请求接收者以其他手段来处理与消息相关的方法调用。这又细分为两小步。首先,请接收者看看有没有其他对象能处理这条消息。若有,则运行期系统会把消息转给那个对象,于是消息转发过程结束,一切如常。若没有“备援的接收者”(replacement receiver),则启动完整的消息转发机制,运行期系统会把与消息有关的全部细节都封装到NSInvocation对象中,再给接收者最后一次机会,令其设法解决当前还未处理的这条消息。

动态方法解析

对象在收到无法解读的消息后,首先将调用其所属类的下列类方法:


+ (BOOL)resolvelnstanceMethod:(SEL)selector

该方法的参数就是那个未知的选择子,其返回值为Boolean类型,表示这个类是否能新增一个实例方法用以处理此选择子。在继续往下执行转发机制之前,本类有机会新增一个处理此选择子的方法。假如尚未实现的方法不是实例方法而是类方法,那么运行期系统就会调用另外一个方法,该方法与 “resolvelnstanceMethod:” 类似,叫做 “resolveClassMethod:”。使用这种办法的前提是:相关方法的实现代码已经写好,只等着运行的时候动态插在类里面就可以了。此方案常用来实现@dynamic属性(参见第6条),比如说,要访问CoreData框架中NSManagedObjects对象的属性时就可以这么做,因为实现这些属性所需的存取方法在编译期就能确定。

下列代码演示了如何用“resolvelnstanceMethod:”来实现@dynamic属性:


id autoDictionaryGetter(id self, SEL _cmd);
void autoDictionarySetter(id selff SEL _cmd, id value);
+ (BOOL)resolvelnstanceMethod:(SEL)selector {
    NSString *selectorString = NSStringFromSelector(selector);
    if ( /* selector is from a ©dynamic property ★/ ){
        if([selectorstring has Prefix: @"set"]){
            class addMethod(self,
                            selector,
                            (IMP)autoDictionarySetter,
                            "v@ :@");
    } else {
        class_addMethod(self,
                        selector,
                        (IMP)autoDictionaryGetter,
                        "@ @:");
        }              
        return YES;
    }
return [super resolvelnstanceMethod:selector];

}

备援接受者

当前接收者还有第二次机会能处理未知的选择子,在这一步中,运行期系统会问它:能不能把这条消息转给其他接收者来处理。与该步骤对应的处理方法如下:


-(id)forwardingTargetForSelector:(SEL)selector

方法参数代表未知的选择子,若当前接收者能找到备援对象,则将其返回,若找不到,就返回nil。通过此方案,我们可以用“组合”(composition)来模拟出“多重继承”(multiple inheritance)的某些特性。在一个对象内部,可能还有一系列其他对象,该对象可经由此方法将能够处理某选择子的相关内部对象返回,这样的话,在外界看来,好像是该对象亲自处理了这些消息似的。

请注意,我们无法操作经由这一步所转发的消息。若是想在发送给备援接收者之前先修改消息内容,那就得通过完整的消息转发机制来做了。

完整的消息转发

如果转发算法已经来到这一步的话,那么唯一能做的就是启用完整的消息转发机制了。首先创建NSInvocation对象,把与尚未处理的那条消息有关的全部细节都封于其中。此对象包含选择子、目标(target)及参数。在触发NSIrwocation对象时,“消息派发系统”(message-dispatch system)将亲自出马,把消息指派给目标对象。
此步骤会调用下列方法来转发消息:


-(void)forwardlnvocation:(NSInvocation*)invocation

这个方法可以实现得很简单:只需改变调用目标,使消息在新目标上得以调用即可。然
而这样实现出来的方法与“备援接收者”方案所实现的方法等效,所以很少有人采用这么简
单的实现方式。比较有用的实现方式为:在触发消息前,先以某种方式改变消息内容,比如追加另外一个参数,或是改换选择子,等等。

实现此方法时,若发现某调用操作不应由本类处理,则需调用超类的同名方法。这样的话,继承体系中的每个类都有机会处理此调用请求,直至NSObject。如果最后调用了NSObject类的方法,那么该方法还会继而调用“doesNotRecognizeSelector:”以抛出异常,此异常表明选择子最终未能得到处理。

消息转发全流程

下图展示了消息转发机制处理消息的各个步骤:

接收者在每一步中均有机会处理消息。步骤越往后,处理消息的代价就越大。最好能在第一步就处理完,这样的话,运行期系统就可以将此方法缓存起来了。如果这个类的实例稍后还收到同名选择子,那么根本无须启动消息转发流程。

完整的示例

假设要编写一个类似于“字典”的对象,它里面可以容纳其他对象,只不过开发者要直接通过属性来存取其中的数据。这个类的设计思路是:由幵发者来添加属性定义,并将其声明为@dynamic,而类则会自动处理相关属性值的存放与获取操作。

定义该类的接口为:


#import <Foundation/Foundation.h>
@interface EOCAutoDictionary : NSObject
@property (nonatomic, strong) NSNumber *number;
@property (nonatomic, strong) NSDate *date;
@property (nonatomic, strong)id qpaqueObject;

@end

本例中,这些属性具体是什么其实无关紧要。笔者用了这么多种数据类型,只是想演示此 功能很有用。在类的内部,每个属性的值还是会存放在字典里,所以我们先在类中编写如下代码,并将属性声明为@dynamiC,这样的话,编译器就不会为其自动生成实例变量及存取方法了:


#import "EOCAutoDictionary.h"
#import <objc/runtime.h>

@interface EOCAutoDictionary ()

@property (nonatomic, strong) NSMutableDictionary *backingstore;

@end

@implementation EOCAutoDictionary

@dynamic string, number, date, opaqueObject;

-(id)init {
    if ( (self = [super init]}} {
          _backingStore = [NSMutableDictionary new];
}
    return self;
}

本例的关键在于resolvelnstanceMethod:方法的实现代码:


+ (BOOL)resolvelnstanceMethod:(SEL)selector {
    NSString *selectorstring = NSStringFromSelector(selector); 
    if ([selectorstring hasPrefix: @"set"]){
        class_addMethod(self,
                        selector,
                        (IMP)autoDictionarySetter
                         "v@:@");
    } else {
        class_addMethod(self,
                        selector,
                        (IMP)autoDictionaryGetter,
                        "@@:";)    
    }
    return YES;
}
@end

当开发者首次在EOCAutoDictionary实例上访问某个属性时,运行期系统还找不到 对应的选择子,因为所需的选择子既没有直接实现,也没有合成出来。现在假设要写入 opaqueObject属性,那么系统就会以“setOpaqueObject:”为选择子来调用上面这个方法。 同理,在读取该属性时,系统也会调用上述方法,只不过传入的选择子是opaqueObject

resolvelnslanceMethod方法会判断选择子的前缀是否为set,以此分辨其是set选择子还是 get选择子。在这两种情况下,都要向类中新增一个处理该选择子所用的方法,这两个方 法分别以autoDictionarySetterautoDictionaryGetter函数指针的形式出现。此时就用到class_addMethod方法,它可以向类中动态地添加方法,用以处理给定的选择子。第三个参 数为函数指针,指向待添加的方法。而最后一个参数则表示待添加方法的“类型编码”(type encoding)。在本例中,编码开头的字符表示方法的返回值类型,后续字符则表示其所接受的各个参数.

getter函数可以用下列代码实现:


id autoDictionaryGetter(id self, SEL _cmd) {
    //Get the backing store from the object
    EOCAutoDictionary *typedSelf = (EOCAutoDictionary^)self;
    NSMutableDictionary *backingStore = typedSelf.backingStore;
    //The key is simply the selector name 
    NSString *key = NSStringFromSelector(_cmd);
    // Return the value
    return [backingStore objectForKey:key];
}

而setter函数则可以这么写:


void autoDictionarySetter(id self, SEL _cmd, id value) {
    //Get the backing store from the object
    EOCAutoDictionary *typedSelf = (EOCAutoDictionary*)self;
    NSMutableDictionary *backingStore = typedSelf.backingStore;

    /** The selector will be for example, "setOpaqueObject:"
    *   We need to remove the "set",and lowercase the first
    *   letter of the remainder.
    */
    
    NSString *selectorstring = NSStringFromSelector(_cmd);
    NSMutablestring *key = [selectorstring mutableCopy】;
    // Remove the " : " at the end 
    [key deleteCharactersInRange:NSMakeRange(key.length - 1, 1)];
    // Remove the fsetf prefix
    [key deleteCharactersInRange:NSMakeRange(0, 3)];
    // Lowercase the first character
    NSString *lowercaseFirstChar = [[key substringToIndex:1] lowercasestring];
    [key replaceCharactersInRange:NSMakeRange(0 ,1) withString:lowercaseFirstChar];
    if (value) {
    [backingStore setObject:value forKey:key];
    } else {
    [backingStore removeObjectForKey:key];
    }
}

EOCAutoDictionary的用法很简单:


    EOCAutoDictionary *dict = [EOCAutoDictionarynew];
    diet.date = [NSDatedateWithTimeIntervalSincel970:475372800];
    NSLog (@"diet .date = %@" , dict.date);
    // Output: diet.date = 1985-01-24 00:00:00 +0000

其他属性的访问方式与date类似,要想添加新属性,只需来定义,并将其声明为@dynamic即可。

要点

  • 若对象无法响应某个选择子,则进人消息转发流程。
  • 通过运行期的动态方法解析功能,我们可以在需要用到某个方法时再将其加入类中。
  • 对象可以把其无法解读的某些选择子转交给其他对象来处理。
  • 经过上述两步之后,如果还是没办法处理选择子,那就启动完整的消息转发机制。