首页 > ios > 第12条:理解消息转发机制(2)

第12条:理解消息转发机制(2)

完整的消息转发

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

- (void)forwardInvocation:(NSInvocation*)invocation 

  这个方法可以实现得很简单:只需改变调用目标,使消息在新目标上得以调用即可。然而这样实现出来的方法与“备援接收者”方案所实现的方法等效,所以很少有人采用这么简单的实现方式。比较有用的实现方式为:在触发消息前,先以某种方式改变消息内容,比如追加另外一个参数,或是改换选择子,等等。
  实现此方法时,若发现某调用操作不应由本类处理,则需调用超类的同名方法。这样的话,继承体系中的每个类都有机会处理此调用请求,直至NSObject。如果最后调用了NSObject类的方法,那么该方法还会继而调用“doesNotRecognizeSelector:”以抛出异常,此异常表明选择子最终未能得到处理。

消息转发全流程

  图2-2这张流程图描述了消息转发机制处理消息的各个步骤。

消息转发机制
  接收者在每一步中均有机会处理消息。步骤越往后,处理消息的代价就越大。最好能在第一步就处理完,这样的话,运行期系统就可以将此方法缓存起来了。如果这个类的实例稍后还收到同名选择子,那么根本无须启动消息转发流程。若想在第三步里把消息转给备援的接收者,那还不如把转发操作提前到第二步。因为第三步只是修改了调用目标,这项改动放在第二步执行会更为简单,不然的话,还得创建并处理完整的NSInvocation。
  以完整的例子演示动态方法解析
  为了说明消息转发机制的意义,下面示范如何以动态方法解析来实现@dynamic属性。假设要编写一个类似于“字典”的对象,它里面可以容纳其他对象,只不过开发者要直接通过属性来存取其中的数据。这个类的设计思路是:由开发者来添加属性定义,并将其声明为@dynamic,而类则会自动处理相关属性值的存放与获取操作。怎么样,这项功能听起来不错吧?
  该类的接口可以写成:

#import <Foundation/Foundation.h> 
 
@interface EOCAutoDictionary : NSObject  
@property (nonatomic, strong) NSString *string;  
@property (nonatomic, strong) NSNumber *number;  
@property (nonatomic, strong) NSDate *date;  
@property (nonatomic, strong) id opaqueObject;  
@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;  
} 

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

+ (BOOL)resolveInstanceMethod:(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。resolveInstanceMethod方法会判断选择子的前缀是否为set,以此分辨其是set选择子还是get选择子。在这两种情况下,都要向类中新增一个处理该选择子所用的方法,这两个方法分别以autoDictionarySetter及autoDictionaryGetter函数指针的形式出现。此时就用到了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 'set' 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 = [EOCAutoDictionary new];  
dict.date = [NSDate dateWithTimeIntervalSince1970:475372800];  
NSLog(@"dict.date = %@", dict.date);  
// Output: dict.date = 1985-01-24 00:00:00 +0000 

  其他属性的访问方式与date类似,要想添加新属性,只需用@property来定义,并将其声明为@dynamic即可。在iOS的CoreAnimation框架中,CALayer类就用了与本例相似的实现方式,这使得CALayer成为“兼容于键值编码的”(key-value-coding-compliant)容器类,也就等于说,能够向里面随意添加属性,然后以键值对的形式来访问。于是,开发者就可以向其中新增自定义的属性了,这些属性值的存储工作由基类直接负责,我们只需在CALayer的子类中定义新属性即可。

要点

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

  1. 还没有评论
评论提交中, 请稍候...

留言


可以使用的标签: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>
Trackbacks & Pingbacks ( 0 )
  1. 还没有 trackbacks