深入浅出Cocoa之消息(二)-详解动态方法决议(Dynamic Method Resolution) 【转】
序言
如果我们在 Objective C 中向一个对象发送它无法处理的消息,会出现什么情况呢?根据前文《深入浅出Cocoa之消息》的介绍,我们知道发送消息是通过 objc_send(id, SEL, ...) 来实现的,它会首先在对象的类对象的 cache,method list 以及父类对象的 cache, method list 中依次查找 SEL 对应的 IMP;如果没有找到且实现了动态方法决议机制就会进行决议,如果没有实现动态方法决议机制或决议失败且实现了消息转发机制就会进入消息转发流程,否则程序 crash。也就是说如果同时提供了动态方法决议和消息转发,那么动态方法决议先于消息转发,只有当动态方法决议依然无法正确决议 selector 的实现,才会尝试进行消息转发。在前文中,我并没有详细讲解动态方法决议,因此本文将详细介绍之。
本文代码下载:点此下载
一,向一个对象发送该对象无法处理的消息
如下代码:
@interface Foo : NSObject -(void)Bar; @end @implementation Foo -(void)Bar { NSLog(@" >> Bar() in Foo"); } @end ///////////////////////////////////////////////// #import "Foo.h" int main (int argc, const char * argv[]) { @autoreleasepool { Foo * foo = [[Foo alloc] init]; [foo Bar]; [foo MissMethod]; [foo release]; } return 0; }
在编译时,XCode 会提示警告:
Instance method '-MissMethod' not found (return type defaults to 'id')
如果,我们忽视该警告运行之,一定会 crash:
>> Bar() in Foo
-[Foo MissMethod]: unrecognized selector sent to instance 0x10010c840
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Foo MissMethod]: unrecognized selector sent to instance 0x10010c840'
*** Call stack at first throw:
......terminate called after throwing an instance of 'NSException'
下划线部分就是造成 crash 的原因:对象无法处理 MissMethod 对应的 selector,也就是没有相应的实现。
二,动态方法决议
Objective C 提供了一种名为动态方法决议的手段,使得我们可以在运行时动态地为一个 selector 提供实现。我们只要实现 +resolveInstanceMethod: 和/或 +resolveClassMethod: 方法,并在其中为指定的 selector 提供实现即可(通过调用运行时函数 class_addMethod 来添加)。这两个方法都是 NSObject 中的类方法,其原型为:
+ (BOOL)resolveClassMethod:(SEL)name;
+ (BOOL)resolveInstanceMethod:(SEL)name;
参数 name 是需要被动态决议的 selector;返回值文档中说是表示动态决议成功与否。但在上面的例子中(不涉及消息转发的情况下),如果在该函数内为指定的 selector 提供实现,无论返回 YES 还是 NO,编译运行都是正确的;但如果在该函数内并不真正为 selector 提供实现,无论返回 YES 还是 NO,运行都会 crash,道理很简单,selector 并没有对应的实现,而又没有实现消息转发。resolveInstanceMethod 是为对象方法进行决议,而 resolveClassMethod 是为类方法进行决议。
下面我们用动态方法决议手段来修改上面的代码:
// // Foo.m // DeepIntoMethod // // Created by 飘飘白云 on 12-11-13. // Copyright (c) 2012年 kesalin@gmail.com All rights reserved. // #import "Foo.h" #include <objc/runtime.h> void dynamicMethodIMP(id self, SEL _cmd) { NSLog(@" >> dynamicMethodIMP"); } @implementation Foo -(void)Bar { NSLog(@" >> Bar() in Foo"); } + (BOOL)resolveInstanceMethod:(SEL)name { NSLog(@" >> Instance resolving %@", NSStringFromSelector(name)); if (name == @selector(MissMethod)) { class_addMethod([self class], name, (IMP)dynamicMethodIMP, "v@:"); return YES; } return [super resolveInstanceMethod:name]; } + (BOOL)resolveClassMethod:(SEL)name { NSLog(@" >> Class resolving %@", NSStringFromSelector(name)); return [super resolveClassMethod:name]; } @end
在前文《深入浅出Cocoa之消息》中已经介绍过 Objective C 中的方法其实就是至少带有两个参数(self 和 _cmd)的普通 C 函数,因此在上面的代码中提供这样一个 C 函数 dynamicMethodIMP,让它来充当对象方法 MissMethod 这个 selector 的动态实现。因为 MissMethod 是被对象所调用,所以它被认为是一个对象方法,因而应该在 resolveInstanceMethod 方法中为其提供实现。通过调用
class_addMethod([ self class ], name, (IMP)dynamicMethodIMP, "v@:" ); |
就能在运行期动态地为 name 这个 selector 添加实现:dynamicMethodIMP。class_addMethod 是运行时函数,所以需要导入头文件:objc/runtime.h。
再次编译运行前面的测试代码,输出如下:
>> Bar() in Foo.
>> Instance resolving MissMethod
>> dynamicMethodIMP called.
>> Instance resolving _doZombieMe
dynamicMethodIMP 被调用了,crash 没有了!万事大吉!
注意:这里两次调用了 resolveInstanceMethod,而且两次决议的 selector 在不同的系统下是不同的,上面演示的是 10.7 系统下第一个决议 MissMethod,第二个决议 _doZombieMe;在 10.6 系统下两次都是决议 MissMethod。
下面我把 resolveInstanceMethod 方法中为 selector 添加实现的那一行屏蔽了,消息转发就应该会进行:
//class_addMethod([self class], name, (IMP)dynamicMethodIMP, "v@:");
再次编译运行,此时输出:
>> Bar() in Foo.
>> Instance resolving MissMethod
+[Foo resolveInstanceMethod:MissMethod] returned YES, but no new implementation of -[Foo MissMethod] was found
>> Instance resolving _doZombieMe
objc[1223]: +[Foo resolveInstanceMethod:MissMethod] returned YES, but no new implementation of -[Foo MissMethod] was found
-[Foo MissMethod]: unrecognized selector sent to instance 0x10010c880
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Foo MissMethod]: unrecognized selector sent to instance 0x10010c880'
*** Call stack at first throw:
......
在这里,resolveInstanceMethod 使诈了,它声称成功(返回 YES )决议了 selector,但是并没有真正提供实现,被编译器发觉而提示相应的错误信息。那它的返回值到底有什么作用呢,在它没有提供真正的实现,并且提供了消息转发机制的情况下,YES 表示不进行后续的消息转发,返回 NO 则表示要进行后续的消息转发。
三,源码剖析
让我们来看看运行时系统是如何进行动态方法决议的,下面的代码来自苹果官方公开的源码 objc-class.mm,我在其中添加了中文注释:
1,首先是判断是不是要进行类方法决议,如果不是或决议失败,则进行实例方法决议(请参考:《深入浅出Cocoa之类与对象》):
/*********************************************************************** * _class_resolveMethod * Call +resolveClassMethod or +resolveInstanceMethod and return * the method added or NULL. * Assumes the method doesn't exist already. **********************************************************************/ __private_extern__ Method _class_resolveMethod(Class cls, SEL sel) { Method meth = NULL; if (_class_isMetaClass(cls)) { meth = _class_resolveClassMethod(cls, sel); } if (!meth) { meth = _class_resolveInstanceMethod(cls, sel); } if (PrintResolving && meth) { _objc_inform("RESOLVE: method %c[%s %s] dynamically resolved to %p", class_isMetaClass(cls) ? '+' : '-', class_getName(cls), sel_getName(sel), method_getImplementation(meth)); } return meth; }
2,类方法决议与实例方法决议大体相似,在这里就只看实例方法决议部分了:
/*********************************************************************** * _class_resolveInstanceMethod * Call +resolveInstanceMethod and return the method added or NULL. * cls should be a non-meta class. * Assumes the method doesn't exist already. **********************************************************************/ static Method _class_resolveInstanceMethod(Class cls, SEL sel) { BOOL resolved; Method meth = NULL; // 是否实现了 resolveInstanceMethod,如果没有返回 NULL if (!look_up_method(((id)cls)->isa, SEL_resolveInstanceMethod, YES /*cache*/, NO /*resolver*/)) { return NULL; } // 调用 resolveInstanceMethod,并获取返回值 resolved = ((BOOL(*)(id, SEL, SEL))objc_msgSend)((id)cls, SEL_resolveInstanceMethod, sel); if (resolved) { // 返回值为 YES,表示 resolveInstanceMethod 声称它已经成功添加实现,则再次查找 method list // +resolveClassMethod adds to self meth = look_up_method(cls, sel, YES/*cache*/, NO/*resolver*/); if (!meth) { // resolveInstanceMethod 使诈了,它声称成功添加实现了,但实际没有,给出警告信息,并返回 NULL // Method resolver didn't add anything? _objc_inform("+[%s resolveInstanceMethod:%s] returned YES, but " "no new implementation of %c[%s %s] was found", class_getName(cls), sel_getName(sel), class_isMetaClass(cls) ? '+' : '-', class_getName(cls), sel_getName(sel)); return NULL; } } // 其他情况下返回 NULL return meth; }
这段代码很容易理解:
1,首先判断是否实现了 resolveInstanceMethod,如果没有实现,返回 NULL,进入下一步处理;
2,如果实现了,调用 resolveInstanceMethod,获取返回值;
3,如果返回值为 YES,表示 resolveInstanceMethod 声称它已经提供了 selector 的实现,因此再次查找 method list,如果依然找到对应的 IMP,则返回该实现,否则提示警告信息,返回 NULL,进入下一步处理;
4,如果返回值为 NO,返回 NULL,进入下一步处理;
四,加入消息转发
在前文《深入浅出Cocoa之消息》一文中,我演示了一个消息转发的示例,下面我把动态方法决议部分去除,把消息转发部分添加进来:
// Proxy @interface Proxy : NSObject -(void)MissMethod; @end @implementation Proxy -(void)MissMethod { NSLog(@" >> MissMethod() called in Proxy."); } @end // Foo @interface Foo : NSObject -(void)Bar; @end @implementation Foo - (void)forwardInvocation:(NSInvocation *)anInvocation { SEL name = [anInvocation selector]; NSLog(@" >> forwardInvocation for selector %@", NSStringFromSelector(name)); Proxy * proxy = [[[Proxy alloc] init] autorelease]; if ([proxy respondsToSelector:name]) { [anInvocation invokeWithTarget:proxy]; } else { [super forwardInvocation:anInvocation]; } } - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { return [Proxy instanceMethodSignatureForSelector:aSelector]; } -(void)Bar { NSLog(@" >> Bar() in Foo."); } @end
运行测试代码,输出如下:
>> Bar() in Foo.
>> forwardInvocation for selector MissMethod
>> MissMethod() called in Proxy.
如果我把动态方法决议部分代码也加入进来输出又是怎样呢?下面只列出了 Foo 的实现代码,其他代码不变动。
@implementation Foo +(BOOL)resolveInstanceMethod:(SEL)name { NSLog(@" >> Instance resolving %@", NSStringFromSelector(name)); if (name == @selector(MissMethod)) { class_addMethod([self class], name, (IMP)dynamicMethodIMP, "v@:"); return YES; } return [super resolveInstanceMethod:name]; } +(BOOL)resolveClassMethod:(SEL)name { NSLog(@" >> Class resolving %@", NSStringFromSelector(name)); return [super resolveClassMethod:name]; } - (void)forwardInvocation:(NSInvocation *)anInvocation { SEL name = [anInvocation selector]; NSLog(@" >> forwardInvocation for selector %@", NSStringFromSelector(name)); Proxy * proxy = [[[Proxy alloc] init] autorelease]; if ([proxy respondsToSelector:name]) { [anInvocation invokeWithTarget:proxy]; } else { [super forwardInvocation:anInvocation]; } } - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { return [Proxy instanceMethodSignatureForSelector:aSelector]; } -(void)Bar { NSLog(@" >> Bar() in Foo."); } @end
此时,输出为:
>> Bar() in Foo.
>> Instance resolving MissMethod
>> dynamicMethodIMP called.
>> Instance resolving _doZombieMe
注意到了没,消息转发没有进行!在前文中说过,消息转发只有在对象无法正常处理消息时才会调用,而在这里我在动态方法决议中为 selector 提供了实现,使得对象可以处理该消息,所以消息转发不会继续了。官方文档中说:
If you implement
resolveInstanceMethod:
but want particular selectors to actually be forwarded via the forwarding mechanism, you returnNO
for those selectors.
文档里的说法其实并不准确,只有在 resolveInstanceMethod 的实现中没有真正为 selector 提供实现,并返回 NO 的情况下才会进入消息转发流程;否则绝不会进入消息转发流程,程序要么调用正确的动态方法,要么 crash。这也与前面的源码不太一致,我猜测在比上面源码的更高层次的地方,再次查找了 method list,如果提供了实现就能够找到该实现。
下面我把 resolveInstanceMethod 方法中为 selector 添加实现的那一行屏蔽了,消息转发就应该会进行:
//class_addMethod([self class], name, (IMP)dynamicMethodIMP, "v@:");
再次编译运行,此时输出正如前面所推断的那样:
>> Bar() in Foo.
>> Instance resolving MissMethod
objc[1618]: +[Foo resolveInstanceMethod:MissMethod] returned YES, but no new implementation of -[Foo MissMethod] was found
>> forwardInvocation for selector MissMethod
>> MissMethod() called in Proxy.
>> Instance resolving _doZombieMe
进行了消息转发!而且编译器很善意地提示(见前面源码剖析):哎呀,你不能欺骗我嘛,你说添加了实现(返回YES),其实还是没有呀!然后编译器就无奈地去看能不能消息转发了。当然如果把返回值修改为 NO 就不会有该警告出现,其他的输出不变。
五,总结
从上面的示例演示可以看出,动态方法决议是先于消息转发的。
如果向一个 Objective C 对象对象发送它无法处理的消息(selector),那么编译器会按照如下次序进行处理:
1,首先看是否为该 selector 提供了动态方法决议机制,如果提供了则转到 2;如果没有提供则转到 3;
2,如果动态方法决议真正为该 selector 提供了实现,那么就调用该实现,完成消息发送流程,消息转发就不会进行了;如果没有提供,则转到 3;
3,其次看是否为该 selector 提供了消息转发机制,如果提供了消息了则进行消息转发,此时,无论消息转发是怎样实现的,程序均不会 crash。(因为消息调用的控制权完全交给消息转发机制处理,即使消息转发并没有做任何事情,运行也不会有错误,编译器更不会有错误提示。);如果没提供消息转发机制,则转到 4;
4,运行报错:无法识别的 selector,程序 crash;
六,引用
官方运行时源代码:http://www.opensource.apple.com/source/objc4/objc4-532/runtime/