Notes

处理未知消息

向对象发送消息,如果没有在对象的模型上找到方法,系统会利用「消息转发」机制来动态处理这个问题

处理方式及对应涉及的方法

例子模型

  1. Human, 继承自 NSObject
  2. Adam, 继承自 Human
  3. Eve, 继承自 Human
  4. Gain, 继承自 Human

Human

@interface Human: NSObject
- (void)eat;
- (void)sleep;
@end

@implementation Human

- (void)eat {
    NSLog(@"%@ eats", self);
}

- (void)sleep {
    NSLog(@"%@ sleeps", self);
}

@end

Adam

@interface Adam: Human
- (void)work;
@end

@implementation Adam

- (void)work {
    NSLog(@"%@ works", self);
}

@end

Eve

@interface Eve: Human
- (void)birth;
@end

@implementation Eve

- (void)birth {
    NSLog(@"%@ births", self);
}

Gain

@interface Gain: Human
- (void)slay;
@end

@implementation Gain
- (void)slay {
    NSLog(@"%@ slayed Abel", self);
}
@end

动态解析

动态解析,实际上还没有到达传说中的「消息转发」的地步,只是看能不能动态地给类添加方法,应用事例:使用 @dynamic 修饰的属性

参数

sel 未能处理的 selector

返回值

BOOL 是否添加了新方法来处理 selector

应用

Human *human = [[Human alloc] init];
[human eat];
[human sleep];
    
Adam *adam = [[Adam alloc] init];
[adam eat];
[adam sleep];
[adam work];

// 向 human 发送 work 消息,不进行方法动态解析的情况下,这会崩溃
[human performSelector:@selector(work)];

添加重定向的方法

void workSomething(id self, SEL _cmd) {
    NSLog(@"method resolved");
    NSLog(@"class: %@, selector: %@", self, NSStringFromSelector(_cmd));
//    NSLog(@"class: %@, selector: %s", self, sel_getName(_cmd)); // 与上一句效果一样
}

Human 上重写动态解析相关方法

#include <objc/runtime.h>

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSString *selector = NSStringFromSelector(sel);
    if ([selector isEqualToString:@"work"]) {
        class_addMethod(self, sel, (IMP)workSomething, "v@:");
        return YES;
    }
    
    return [super resolveClassMethod:sel];
}

使用 Runtime 方法前,先引入这个文件 在这个例子中,class_addMethod 是 Runtime 的方法

class_addMethod 方法最后的参数解析
每个方法默认隐藏两个参数:self -> 调用者,_cmd -> 方法的 SEL
v@:
v -> 函数返回值为 void
@ -> self
-> _cmd

消息转发

不实现方法的动态解析,也可以直接消息转发

消息转发也有两种实现

只替换消息接收对象

- (id)forwardingTargetForSelector:(SEL)aSelector

参数

aSelector 未能处理的 SEL

返回值

id 重新接收消息的对象

应用

Human *human = [[Human alloc] init];
[human eat];
[human sleep];

Eve *eve= [[Eve alloc] init];
[eve eat];
[eve sleep];
[eve birth];

// 向 human 发送 birth 消息,没有实现消息转发的情况下,这会崩溃
[human performSelector:@selector(birth)];

Human 中添加实现消息转发的方法

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if ([Eve instancesRespondToSelector:aSelector]) {
        return [[Eve alloc] init];
    }
    
    return [super forwardingTargetForSelector:aSelector];
}

完整的消息转发

相关的类与方法

关于方法调用时机

在「只替换消息转发对象」的环节中,失败之后,Runtime 将会调用 - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel; 创建 NSInvocation. 然后调用 - (void)forwardInvocation:(NSInvocation *)anInvocation;, 在此方法中,会对新创建的 NSInvocation 调用 - (void)invokeWithTarget:(id)anObject; 方法,在这个方法中,会对新接收者(anObject)调用同名方法(那个未能处理的方法)

需要重写的方法

需要手动调用的方法

应用

Human *human = [[Human alloc] init];
[human eat];
[human sleep];

Gain *gain = [[Gain alloc] init];
[gain eat];
[gain sleep];
[gain slay];

// 向 human 发送 work 消息,不进行消息转发的情况下,这会崩溃
[human performSelector:@selector(slay)];

Human 中实现消息转发的方法

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    // 先看看父类有没有进行消息转发
    NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
    if (!signature) {
        if ([Gain instancesRespondToSelector:aSelector]) {
            signature = [Gain instanceMethodSignatureForSelector:aSelector];
        }
    }
    
    return signature;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = anInvocation.selector;
    if ([Gain instancesRespondToSelector:sel]) {
        Gain *gain = [[Gain alloc] init];
        [anInvocation invokeWithTarget:gain];
    } else {
        [self doesNotRecognizeSelector:sel];
    }
}

References