前言
在Objective-C中,如果只在头文件中声明了方法,但没有在m文件中实现该方法,如果调用该方法,通常情况下程序会崩溃并抛出unrecognized selector sent to instance的异常。
而在异常抛出之前,Runtime会给你三次拯救程序的机会:
- Method resolution
- Fast forwarding
- Normal forwarding
下图是动态方法解析和消息转发的图
1. Method resolution
Runtime首先给我们提供的一次机会,动态方法解析,可以让我们在运行时完成向特定类添加特定方法实现的操作。如下例子:
@interface TestClass : NSObject
- (int)print:(int)count;
+ (void)classPrint;
@end
在TestClass类中,我们声明了一个类方法classPrint和实例方法print,但是,并没有在m文件中实现他们。此时编译器会出现警告
Method definition for 'print:' not found
Method definition for 'classPrint' not found
如果在代码中调用这两个方法,编译不会出错,但运行的时候会出错,也就是抛出unrecognized selector sent to instance的异常。
我们使用动态方法解析来在运行时为TestClass类添加这两个方法的实现,其中类方法通过resolveClassMethod来添加,实例方法通过resolveInstanceMethod来添加。
int missingPrint(id obj, SEL _cmd, int count)
{
NSLog(@"Wow, you found a missing method and count is %d", count);
return count * 100;
}
void missingClassPrint(id obj, SEL _cmd)
{
NSLog(@"调用了missingClassPrint函数");
}
@implementation TestClass
+ (BOOL)resolveClassMethod:(SEL)sel
{
NSLog(@"调用resolveClassMethod!!!");
if(sel == @selector(classPrint)) {
class_addMethod(object_getClass(self), sel, (IMP)missingClassPrint, "v@:");
return YES;
}
return [class_getSuperclass(self) resolveClassMethod:sel];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSLog(@"调用resolveInstanceMethod!!!");
if(sel == @selector(print:)) {
class_addMethod([self class], sel, (IMP)missingPrint, "I@:I");//其中v@:表示方法的参数和返回值,详见Type Encodings
return YES;
}
return [super resolveInstanceMethod:sel];
}
@end
上面代码中,我们定义了两个C函数,missingPrint函数用于绑定实例方法,missingClassPrint用于绑定类方法。在resolveClassMethod方法和resolveInstanceMethod方法中,通过class_addMethod方法为类动态添加方法实现。
如此一来,调用classPrint和print:这两个方法时,Runtime会解析成对应的missingClassPrint方法和missingPrint方法。
int main(int argc, const char * argv[])
{
TestClass *t = [[TestClass alloc] init];
[TestClass classPrint];//这里调用classPrint类方法
int res = [t print:55];//这里调用print实例方法
NSLog(@"res = %d", res);
return 0;
}
PS 1:class_addMethod方法的第三个参数,"v@:",描述的是方法的返回值和传参,该符号的具体内容可在Type Encodings查看
PS 2:resolveClassMethod和resolveInstanceMethod方法都是类方法,在类方法中的self,实际上是Class对象,而不是实例对象,只能调用类方法,不能调用实例方法。
2. Fast forwarding
若在第一步中,类中没有实现动态解析方法,或者在方法中返回NO,则会走到Fast forwarding这一步。
对于实例方法,需要实现forwardingTargetForSelector方法
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if(aSelector == @selector(print:)) {
return [MessageForwarding new];//返回另外一个对象
//return self //返回self会导致到第三步Normal forwarding
}
return nil;
}
这里可以看到,只要捕捉到方法选择器是print方法,则返回一个新建的MessageForwarding对象,将该消息重定向到MessageForwarding对象来处理。这里MessageForwarding类只需声明并实现了print方法即可。
若想转发类方法,则需要重写forwardingTargetForSelector的类方法
+ (id)forwardingTargetForSelector:(SEL)aSelector
{
if(aSelector == @selector(classPrint)) {
return [MessageForwarding class];//这里的返回和实例方法中不一样
}
return nil;
}
这里返回的是[MessageForwarding class],因为调用类方法需要的是一个类对象,而不是实例对象。
3.Normal forwarding
当前两步都没有实现,或者在Fast forwarding这一步的forwardingTargetForSelector方法中返回self,都会进入Normal forwarding这一步。
首先会发送-methodSignatureForSelector:
消息获得函数的参数和返回类型。接着Runtime会创建一个NSInvocation对象并发送-forwardInvocation:
消息给目标对象。
NSInvocation 实际上就是对一个消息的描述,包括selector 以及参数等信息。
这里需要我们自己实现这两个方法。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];
if(!methodSignature){
methodSignature = [NSMethodSignature signatureWithObjCTypes:"I@:I"];
}
return methodSignature;
}
//NSInvocation由NSMethodSignature生成
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
NSLog(@"calling forwardInvocation: method");
MessageForwarding *mf = [MessageForwarding new];
if([mf respondsToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:mf];
}
}
这里仍然是将消息转发给MessageForwarding对象来处理。
总结
Objective-C 中给一个对象发送消息会经过以下几个步骤:
- 在对象类的 dispatch table 中尝试找到该消息。如果找到了,跳到相应的函数IMP去执行实现代码;
- 如果没有找到,Runtime 会发送
+resolveInstanceMethod:
或者+resolveClassMethod:
尝试去 resolve 这个消息; - 如果 resolve 方法返回 NO,Runtime 就发送
-forwardingTargetForSelector:
允许你把这个消息转发给另一个对象; - 如果没有新的目标对象返回, Runtime 就会发送
-methodSignatureForSelector:
和-forwardInvocation:
消息。你可以发送-invokeWithTarget:
消息来手动转发消息或者发送-doesNotRecognizeSelector:
抛出异常。
参考文章
Objective-C Runtime
Objective-C Runtime By 杨萧玉