Objective-C 是一门动态语言,它的动态性体现在它将很多编译和链接时做的事推延到运行时处理,而这一机制主要依赖系统提供的 runtime 库。利用 runtime 库,我们能在运行时做很多事,例如 objc_setAssociatedObject 动态绑定属性、method swizzling、class_copyIvarList 动态获取属性实现 ORM(Object Relational Mapping)、消息转发等,本文先解析消息转发机制。
消息分发
我们知道,Objective-C 中的方法调用最终都是执行消息发送,如
[receiver message]
最后会转换成
objc_msgSend(receiver, selector)
如果 receiver 不响应 message 消息,则会去父类查找,父类还不响应继续去父类查找,如果一直查找到 NSObject 还不响应,则会进入消息动态处理流程。
消息动态处理
如果一个类不响应某个具体的方法,在进入消息转发流程前还有两个时机处理,
- resolveInstanceMethod
+ (BOOL)resolveInstanceMethod:(SEL)sel;
在这个方法中我们可以利用runtime的特性动态添加方法来处理,具体如下:
void dynamicMethodIMP(id self, SEL _cmd)
{
// implementation ....
}
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSLog(@"sel is %@", NSStringFromSelector(sel));
if(sel == @selector(setName:)){
class_addMethod([self class],sel,(IMP)dynamicMethodIMP,"v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
这种方式在CoreData中有使用到,做法就是将property设置为@dynamic
- forwardingTargetForSelector
- (id)forwardingTargetForSelector:(SEL)aSelector
{
//如果代理对象能处理,则转接给代理对象
if ([proxyObj respondsToSelector:aSelector]) {
return proxyObj;
}
//不能处理进入转发流程
return nil;
}
消息转发
如果上述两个时机都无法处理消息,则会进入消息转发流程,这个流程的关键方法是:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
- (void)forwardInvocation:(NSInvocation *)anInvocation
注意:如果 methodSignatureForSelector 返回的NSMethodSignature 是 nil 的话不会继续执行 forwardInvocation,转发流程终止,抛出无法处理的异常。
如果 methodSignatureForSelector 返回了方法签名,我们还有最后一次机会处理这个消息,那就是在 forwardInvocation 回调里,具体可以按照如下的方式使用:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
NSMethodSignature *sig = [BBMessageForwardProxy instanceMethodSignatureForSelector:@selector(bb_dealNotRecognizedMessage:)];
return sig;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
NSString *debugInfo = [NSString stringWithFormat:@"[debug]unRecognizedMessage:[%@] sent to [%@]",NSStringFromSelector(anInvocation.selector),NSStringFromClass([self class])];
//重定向方法
[anInvocation setSelector:@selector(bb_dealNotRecognizedMessage:)];
//传递调用信息
[anInvocation setArgument:&debugInfo atIndex:2];
//BBMessageForwardProxy对象接收转发的消息并打印调用信息
[anInvocation invokeWithTarget:[BBMessageForwardProxy new]];
}
BBMessageForwardProxy.h
#import <Foundation/Foundation.h>
@interface BBMessageForwardProxy : NSObject
- (void)bb_dealNotRecognizedMessage:(NSString *)debugInfo;
@end
BBMessageForwardProxy.m
#import "BBMessageForwardProxy.h"
@implementation BBMessageForwardProxy
- (void)bb_dealNotRecognizedMessage:(NSString *)debugInfo
{
NSLog(@"%@",debugInfo);
}
至此我们在消息转发的最后一个环节里将无法识别的消息转发到了一个 Proxy 类统一打印处理。
整个流程如下图所示:
在每一步,消息接受者都有机会去处理消息。越往后处理代价越高,最好是能在第一步就处理消息,这样 runtime 会在处理完后缓存结果,下回再发送同样消息的时候可以提高处理效率。第二步转移消息的接受者也比进入转发流程的代价要小,毕竟到达最后一步就需要完整地处理 NSInvocation 对象。
这是我写的 runtime 系列文章中的一篇,还有以下几篇从其他方面对 runtime 进行了介绍
- iOS runtime 之 Class 和 MetaClass
- iOS runtime 之 Category
- 深入理解 Objective-C 的方法调用流程
- Objective-C 深入理解 +load 和 +initialize
如果您觉得本文对您有所帮助,请点击「喜欢」来支持我。
转载请注明出处,有任何疑问都可联系我,欢迎探讨。
网友评论
这一步里既然重定向selector 那这个selector是不是可以跟- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 里生成签名的selector不同呢?
作者,想问下,利用消息转发机制,是不是可以做容错处理,比如发送了某个类没有实现的消息后,可以通过消息转发只打印错误信息,不让程序崩溃?当然这样做好像没什么实际意义,作为开发我们是希望有问题时能最直观的看到,例如 crash ~