Effective Objective-C 2.0: Item 12: Understand Message Forwarding

这点很有意思,理清楚了Message Forward的过程(特别那个例子有意思),但是感觉 具体例子太少了~~ 难道是我没有理解透?


Item 12: Understand Message Forwarding

Item 11 explains why it’s important to understand the way messages are sent to objects. Item 12 explores why it’s important to understand what happens when a message is sent to an object that it doesn’t understand.

A class can understand only messages that it has been programmed to understand, through implementing methods. But it’s not a compile-time error to send a message to a class that it doesn’t understand, since methods can be added to classes at runtime so the compiler has no way of knowing whether a method implementation is going to exist. When it receives a method that it doesn’t understand, an object goes through message forwarding, a process designed to allow you as the developer to tell the message how to handle the unknown message.

Even if you are unaware of it, you will very likely already have encountered messages going through the message-forwarding pathways. Every time you’ve seen a message such as the following in the console, it’s because you’ve sent a message to an object that it doesn’t understand and the default NSObject implementation of the forwarding path has kicked in:

-[__NSCFNumber lowercaseString]: unrecognized selector sent to
instance 0x87
*** Terminating app due to uncaught exception
'NSInvalidArgumentException', reason: '-[__NSCFNumber
lowercaseString]: unrecognized selector sent to instance 0x87'

This is an exception being thrown from NSObject’s method calleddoesNotRecognizeSelector: telling you that the receiver of the message is of type__NSCFNumber and that it doesn’t understand the selector lowercaseString. That’s not really surprising in this case, since NSNumber (__NSCFNumber is the internal class used in toll-free bridging—see Item 49—which is created when you allocate anNSNumber). In this case, the forwarding pathways ended in the application’s crashing, but you can hook into the forwarding pathways in your own classes to perform any desired logic instead of crashing.

The forwarding pathways are split into two avenues. The first gives the class to which the receiver belongs a chance to dynamically add a method for the unknown selector. This is called dynamic method resolution. The second avenue is the full forwarding mechanism. If the runtime has made it this far, it knows that there’s no longer a chance for the receiver itself to respond to the selector. So it asks the receiver to try to handle the invocation itself. It does this in two steps. First, it asks whether another object should receive the message instead. If there is, the runtime diverts the message, and everything proceeds as normal. If there is no replacement receiver, the full forwarding mechanism is put into effect, using the NSInvocation object to wrap up the full details about the message that is currently unhandled and gives the receiver one final chance to handle it.

Dynamic Method Resolution

The first method that’s called when a message is passed to an object that it doesn’t understand is a class method on the object’s class:

+ (BOOL)resolveInstanceMethod:(SEL)selector

This method takes the selector that was not found and returns a Boolean to indicate whether an instance method was added to the class that can now handle that selector. Thus, the class is given a second opportunity to add an implementation before proceeding with the rest of the forwarding mechanism. A similar method, called resolveClassMethod:, is called when the unimplemented method is a class method rather than an instance method.

Using this approach requires the implementation of the method to already be available, ready to plug in to the class dynamically. This method is often used to implement @dynamic properties (see Item 6), such as occurs in CoreData for accessing properties of NSManagedObjects, since the methods required to implement such properties can be known at compile time.

Such an implementation of resolveInstanceMethod: for use with @dynamicproperties might look like this:

id autoDictionaryGetter(id selfSEL _cmd);
void autoDictionarySetter(id selfSEL _cmd, id value);

+ (BOOL)resolveInstanceMethod:(SEL)selector {
    NSString *selectorString = NSStringFromSelector(selector);
    if ( /* selector is from a @dynamic property */ ) {
        if ([selectorString hasPrefix:@"set"]) {
            class_addMethod(self,
                            selector,
                            (IMP)autoDictionarySetter,
                            "v@:@");
        } else {
            class_addMethod(self,
                            selector,
                            (IMP)autoDictionaryGetter,
                            "@@:");
        }
        return YES;
    }
    return [super resolveInstanceMethod:selector];
}

The selector is obtained as a string and then checked to see whether it looks like a setter. If it is prefixed with set, it is assumed to be a setter; otherwise, it is assumed to be a getter. In each case, a method is added to the class for the given selector pointing to an implementation defined as a pure C function. In these C functions there would be code to manipulate whatever data structure was being used by the class to store the properties’ data. For example, in the case of CoreData, these methods would talk to the database back end to retrieve or update values accordingly.

Replacement Receiver

The second attempt to handle an unknown selector is to ask the receiver whether a replacement receiver is available to handle the message instead. The method that handles this is:

- (id)forwardingTargetForSelector:(SEL)selector

The unknown selector is passed in, and the receiver is expected to return the object to act as its replacement or nil if no replacement can be found. This method can be used to provide some of the benefits of multiple inheritance by combining its use with composition. An object could own a range of other objects internally that it returns in this method for selectors that they handle, making it look as though it is itself handling them.

Note that there is no way to manipulate the message using this part of the forwarding path. If the message needs to be altered before sending to the replacement receiver, the full forwarding mechanism must be used.

Full Forwarding Mechanism

If the forwarding algorithm has come this far, the only thing left to do is to apply the full forwarding mechanism. This starts by creating an NSInvocation object to wrap up all the details about the message that is left unhandled. This object contains the selector, target, and arguments. An NSInvocation object can be invoked, which causes the message-dispatch system to whir into action and dispatch the message to its target.

The method that gets called to attempt forwarding this way is:

- (void)forwardInvocation:(NSInvocation*)invocation

A simple implementation would change the target of the invocation and invoke it. This would be equivalent to using the replacement receiver method, so such a simple implementation is rarely used. A more useful implementation would be to change the message in some way before invoking it, such as appending another argument or changing the selector.

An implementation of this method should always call its superclass’s implementation for invocations it doesn’t handle. This means that once all superclasses in the hierarchy have been given a chance to handle the invocation, NSObject’s implementation will be called. This invokes doesNotRecognizeSelector: to raise the unhandled selector exception.

The Full Picture

The process through which forwarding is handled can be described by a flow diagram like that shown in Figure 2.2.

Image

Figure 2.2 Message forwarding

At each step, the receiver is given a chance to handle the message. Each step is more expensive than the one before it. The best scenario is that the method is resolved at the first step, since the method that was resolved will end up being cached by the runtime such that forwarding does not have to kick in at all next time the same selector is invoked on an instance of the same class. At the second step, forwarding a message to another receiver is simply an optimization of the third step for the case in which a replacement receiver can be found. In that case, the only thing that needs to be changed about the invocation is the target, which is much simpler to do than the final step, in which a complete NSInvocation needs to be created and handled.

Full Example of Dynamic Method Resolution

To illustrate how forwarding can be useful, the following example shows the use of dynamic method resolution to provide @dynamic properties. Consider an object that allows you to store any object in it, much like a dictionary, but provides access through properties. The idea of the class will be that you can add a property definition and declare it @dynamic, and the class will magically handle storing and retrieving that value. That would be pretty fantastic, right?

The interface for the class will be like this:

#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

It doesn’t particularly matter for this example what the properties are. In fact, I’ve shown a variety of types just to illustrate the power of this feature. Internally, the values for each property will be held in a dictionary, so the start of the implementation of this class would look like the following, including declaring the properties as @dynamic such that instance variables and accessors are not automatically created for them:

#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;
}

Then comes the fun part: the resolveInstanceMethod: implementation:

+ (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

The first time it encounters a call to a property on an instance of EOCAutoDictionarythe runtime will not find the corresponding selector, since they’re not implemented directly or synthesized. For example, if the opaqueObject property is written, the preceding method will be invoked with a selector of setOpaqueObject:. Similarly, if the property is read, it will be invoked with a selector of opaqueObject. The method detects the difference between set and get selectors by checking for a prefix of set. In each case, a method is added to the class for the given selector pointing to a function called either autoDictionarySetter or autoDictionaryGetter, as appropriate. This makes use of the runtime method class_addMethod, which adds a method dynamically to the class for the given selector, with the implementation given as a function pointer. The final parameter in this function is the type encoding of the implementation. The type encoding is made up from characters representing the return type, followed by the parameters that the function takes.

The getter function would then be implemented as follows:

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];
}

Finally, the setter function would be implemented like this:

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 - 11)];

    // Remove the 'set' prefix
    [key deleteCharactersInRange:NSMakeRange(03)];

    // Lowercase the first character
    NSString *lowercaseFirstChar =
        [[key substringToIndex:1lowercaseString];
    [key replaceCharactersInRange:NSMakeRange(01)
                       withString:lowercaseFirstChar];

    if (value) {
        [backingStore setObject:value forKey:key];
    } else {
        [backingStore removeObjectForKey:key];
    }
}

Using EOCAutoDictionary is then a simple matter of the following:

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

The other properties on the dictionary could be accessed just like the date property, and new properties could be introduced by adding a @property definition and declaring it @dynamic. A similar approach is employed by CALayer, part of the CoreAnimation framework on iOS. This approach allows CALayer to be a key-value-coding-compliant container class, meaning that it can store a value against any key.CALayer uses this ability to allow the addition of custom animatable properties whereby the storage of the property values is handled directly by the base class, but the property definition can be added in a subclass.

Things to Remember

Image Message forwarding is the process that an object goes through when it is found to not respond to a selector.

Image Dynamic method resolution is used to add methods to a class at runtime as and when they are used.

Image Objects can declare that another object is to handle certain selectors that it doesn’t understand.

Image Full forwarding is invoked only when no previous way of handling a selector has been found.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值