runtime(三) Objective-C 的消息转发机制与动态添加方法

Posted by Abin's blog on April 16, 2016

在上上一篇博客 runtime系列(一) objc_msgSend 中介绍了运行时的消息传递机制,但是却没有说对象收到消息却无法解读该怎么办。本篇博客就着重介绍当消息传递时无法解读的时候就会启动的 消息转发机制( message forwarding )

开发可能经常会遇到这种情况:

2016-04-20 13:14:07.391 runtime[1096:22076] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[AutoDictionary setDate:]: unrecognized selector sent to instance 0x100302f50'
*** First throw call stack:
(
	0   CoreFoundation                      0x00007fff9f2d94f2 __exceptionPreprocess + 178
	1   libobjc.A.dylib                     0x00007fff90db3f7e objc_exception_throw + 48
	2   CoreFoundation                      0x00007fff9f3431ad -[NSObject(NSObject) doesNotRecognizeSelector:] + 205
	3   CoreFoundation                      0x00007fff9f249571 ___forwarding___ + 1009
	4   CoreFoundation                      0x00007fff9f2490f8 _CF_forwarding_prep_0 + 120
	5   runtime                             0x0000000100001c1c main + 124
	6   libdyld.dylib                       0x00007fff91df85ad start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

这个异常信息是由 NSObjectdoesNotRecognizeSelector: 方法抛出来的,本来是给 AutoDictionary 的一个实例对象发送消息,但是该对象并没有 setDate: 方法,所以消息转发给了 NSObject ,最后抛出异常。

先看下消息处理机制流程图:

消息处理机制流程图

消息转发分为两阶段三步,第一阶段先看接受消息的对象能不能自己处理这个无法解读的消息,这一步可以动态的添加方法去解读接受这个消息;第二阶段是先看看对象自己不能处理这个消息,能不能交给其他对象来进行处理,在这一步如果仍然无法解读消息,那么就会走最后一步:把和消息有关的所有细节封装到一个 NSInvocation 中,再询问一次对象是否能解决。 看下三个方法:

// 询问对象是否自己处理,是返回YES,一般会在这个方法里面动态添加方法
+ (BOOL)resolveInstanceMethod:(SEL)sel;

// 这一步询问对象把消息交给哪个对象来进行处理
- (id)forwardingTargetForSelector:(SEL)aSelector;

// 如果走到这一步的话,就把消息的所有信息封装成 NSInvocation 对象进行 "最后通牒"
- (void)forwardInvocation:(NSInvocation *)anInvocation;

来一段代码示例: 新建一个 AutoDictionary 类,添加一个 NSDate 类型的 date 属性,在实现文件里面用 @dynamic date; 禁止自动生成存取方法,这样当代码中给 AutoDictionary 实例对象的 date属性赋值时就会出现消息无法解读的现象。 .h 文件:

@interface AutoDictionary : NSObject

@property (nonatomic, strong) NSDate *date;

@end

.m 实现文件代码内容:

@interface AutoDictionary()
@property (nonatomic, strong) NSMutableDictionary *backingStore;

/**
 *  该类仅在实现文件 实现了
 *  - (NSDate *)date
 *  - (void)setDate:(NSDate *)date
 *  两个方法,用于处理 AutoDictionary 无法解读的消息
 */
@property (nonatomic, strong) MethodCreator *methodCreator;
@end
@implementation AutoDictionary
//
@dynamic date;
//
- (instancetype)init{
    if (self = [super init]) {
        self.backingStore = [NSMutableDictionary dictionary];
        self.methodCreator = [MethodCreator new];
    }
    return self;
}

#pragma mark - 消息转发机制 :1.动态添加方法 2.后备消息接收者 3.封装NSInvocation,最后通牒
// 3. 封装NSInvocation,最后通牒
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    //
}
// 2. 无法接受消息,选择由谁来接受
- (id)forwardingTargetForSelector:(SEL)aSelector{
    return self.methodCreator;
}
// 1. 动态添加方法
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    NSString *selString = NSStringFromSelector(sel);
    if ([selString hasPrefix:@"set"]) {
        class_addMethod(self, sel, (IMP)autoDictSetter, "");
    }else{
        class_addMethod(self, sel, (IMP)autoDictGetter, "");
    }
    return YES;
}
id autoDictGetter (id self, SEL _cmd){
    AutoDictionary *dict = self;
    NSString *key = NSStringFromSelector(_cmd);
    return [dict.backingStore objectForKey:key];
}
void autoDictSetter (id self, SEL _cmd, id value){
    AutoDictionary *dict = self;
    NSString *selString = NSStringFromSelector(_cmd);
    NSString *key = [selString substringWithRange:NSMakeRange(3, selString.length-4)];
    key = [key lowercaseStringWithLocale:[NSLocale currentLocale]];
    if (value) {
        [dict.backingStore setObject:value forKey:key];
    }else{
        [dict.backingStore removeObjectForKey:key];
    }
}

@end

测试代码:

AutoDictionary *dict = [AutoDictionary new];
dict.date = [NSDate date];
NSLog(@"dict.date = %@",dict.date);