[深入浅出Cocoa]之消息(二)-详解动态方法决议(Dynamic Method Resolution)
?[深入淺出Cocoa]之消息(二)-詳解動態(tài)方法決議(Dynamic Method Resolution)
羅朝輝 (http://www.cnblogs.com/kesalin/)
本文遵循“署名-非商業(yè)用途-保持一致”創(chuàng)作公用協(xié)議序言
如果我們在 Objective C 中向一個對象發(fā)送它無法處理的消息,會出現(xiàn)什么情況呢?根據(jù)前文《深入淺出Cocoa之消息》的介紹,我們知道發(fā)送消息是通過 objc_send(id, SEL, ...) 來實(shí)現(xiàn)的,它會首先在對象的類對象的 cache,method list 以及父類對象的 cache, method list 中依次查找 SEL 對應(yīng)的 IMP;如果沒有找到且實(shí)現(xiàn)了動態(tài)方法決議機(jī)制就會進(jìn)行決議,如果沒有實(shí)現(xiàn)動態(tài)方法決議機(jī)制或決議失敗且實(shí)現(xiàn)了消息轉(zhuǎn)發(fā)機(jī)制就會進(jìn)入消息轉(zhuǎn)發(fā)流程,否則程序 crash。也就是說如果同時提供了動態(tài)方法決議和消息轉(zhuǎn)發(fā),那么動態(tài)方法決議先于消息轉(zhuǎn)發(fā),只有當(dāng)動態(tài)方法決議依然無法正確決議 selector 的實(shí)現(xiàn),才會嘗試進(jìn)行消息轉(zhuǎn)發(fā)。在前文中,我并沒有詳細(xì)講解動態(tài)方法決議,因此本文將詳細(xì)介紹之。
本文代碼下載:點(diǎn)此下載
一,向一個對象發(fā)送該對象無法處理的消息
如下代碼:
@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')如果,我們忽視該警告運(yùn)行之,一定會 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 對應(yīng)的?selector,也就是沒有相應(yīng)的實(shí)現(xiàn)。
?
二,動態(tài)方法決議
Objective C 提供了一種名為動態(tài)方法決議的手段,使得我們可以在運(yùn)行時動態(tài)地為一個 selector 提供實(shí)現(xiàn)。我們只要實(shí)現(xiàn) +resolveInstanceMethod: 和/或?+resolveClassMethod: 方法,并在其中為指定的 selector ?提供實(shí)現(xiàn)即可(通過調(diào)用運(yùn)行時函數(shù)?class_addMethod 來添加)。這兩個方法都是 NSObject 中的類方法,其原型為:
+ (BOOL)resolveClassMethod:(SEL)name; + (BOOL)resolveInstanceMethod:(SEL)name;參數(shù) name 是需要被動態(tài)決議的 selector;返回值文檔中說是表示動態(tài)決議成功與否。但在上面的例子中(不涉及消息轉(zhuǎn)發(fā)的情況下),如果在該函數(shù)內(nèi)為指定的 selector ?提供實(shí)現(xiàn),無論返回 YES 還是 NO,編譯運(yùn)行都是正確的;但如果在該函數(shù)內(nèi)并不真正為 selector 提供實(shí)現(xiàn),無論返回 YES 還是 NO,運(yùn)行都會 crash,道理很簡單,selector 并沒有對應(yīng)的實(shí)現(xiàn),而又沒有實(shí)現(xiàn)消息轉(zhuǎn)發(fā)。resolveInstanceMethod 是為對象方法進(jìn)行決議,而?resolveClassMethod 是為類方法進(jìn)行決議。
下面我們用動態(tài)方法決議手段來修改上面的代碼:
// // 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之消息》中已經(jīng)介紹過?Objective C 中的方法其實(shí)就是至少帶有兩個參數(shù)(self 和 _cmd)的普通 C 函數(shù),因此在上面的代碼中提供這樣一個 C 函數(shù)?dynamicMethodIMP,讓它來充當(dāng)對象方法 MissMethod 這個 selector 的動態(tài)實(shí)現(xiàn)。因為 MissMethod 是被對象所調(diào)用,所以它被認(rèn)為是一個對象方法,因而應(yīng)該在?resolveInstanceMethod 方法中為其提供實(shí)現(xiàn)。通過調(diào)用
class_addMethod([self class], name, (IMP)dynamicMethodIMP, "v@:");就能在運(yùn)行期動態(tài)地為 name 這個 selector 添加實(shí)現(xiàn):dynamicMethodIMP。class_addMethod 是運(yùn)行時函數(shù),所以需要導(dǎo)入頭文件:objc/runtime.h。
再次編譯運(yùn)行前面的測試代碼,輸出如下:
? >> Bar() in Foo. ? >> Instance resolving MissMethod ? >> dynamicMethodIMP called. ? >> Instance resolving _doZombieMedynamicMethodIMP 被調(diào)用了,crash 沒有了!萬事大吉!
注意:這里兩次調(diào)用了?resolveInstanceMethod,而且兩次決議的 selector 在不同的系統(tǒng)下是不同的,上面演示的是 10.7 系統(tǒng)下第一個決議 MissMethod,第二個決議 _doZombieMe;在 10.6 系統(tǒng)下兩次都是決議 MissMethod。
下面我把 resolveInstanceMethod 方法中為 selector 添加實(shí)現(xiàn)的那一行屏蔽了,消息轉(zhuǎn)發(fā)就應(yīng)該會進(jìn)行:
//class_addMethod([self class], name, (IMP)dynamicMethodIMP, "v@:");再次編譯運(yùn)行,此時輸出:
?>> 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,但是并沒有真正提供實(shí)現(xiàn),被編譯器發(fā)覺而提示相應(yīng)的錯誤信息。那它的返回值到底有什么作用呢,在它沒有提供真正的實(shí)現(xiàn),并且提供了消息轉(zhuǎn)發(fā)機(jī)制的情況下,YES 表示不進(jìn)行后續(xù)的消息轉(zhuǎn)發(fā),返回 ?NO 則表示要進(jìn)行后續(xù)的消息轉(zhuǎn)發(fā)。
?
三,源碼剖析
讓我們來看看運(yùn)行時系統(tǒng)是如何進(jìn)行動態(tài)方法決議的,下面的代碼來自蘋果官方公開的源碼 objc-class.mm,我在其中添加了中文注釋:
1,首先是判斷是不是要進(jìn)行類方法決議,如果不是或決議失敗,則進(jìn)行實(shí)例方法決議(請參考:《深入淺出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,類方法決議與實(shí)例方法決議大體相似,在這里就只看實(shí)例方法決議部分了:
/************************************************************************ _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;// 是否實(shí)現(xiàn)了 resolveInstanceMethod,如果沒有返回 NULLif (!look_up_method(((id)cls)->isa, SEL_resolveInstanceMethod, YES /*cache*/, NO /*resolver*/)){return NULL;}// 調(diào)用 resolveInstanceMethod,并獲取返回值resolved = ((BOOL(*)(id, SEL, SEL))objc_msgSend)((id)cls, SEL_resolveInstanceMethod, sel);if (resolved) {// 返回值為 YES,表示 resolveInstanceMethod 聲稱它已經(jīng)成功添加實(shí)現(xiàn),則再次查找 method list // +resolveClassMethod adds to selfmeth = look_up_method(cls, sel, YES/*cache*/, NO/*resolver*/);if (!meth) {// resolveInstanceMethod 使詐了,它聲稱成功添加實(shí)現(xiàn)了,但實(shí)際沒有,給出警告信息,并返回 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;}}// 其他情況下返回 NULLreturn meth; }這段代碼很容易理解:
1,首先判斷是否實(shí)現(xiàn)了?resolveInstanceMethod,如果沒有實(shí)現(xiàn),返回 NULL,進(jìn)入下一步處理;
2,如果實(shí)現(xiàn)了,調(diào)用?resolveInstanceMethod,獲取返回值;
3,如果返回值為 YES,表示?resolveInstanceMethod 聲稱它已經(jīng)提供了 selector 的實(shí)現(xiàn),因此再次查找 method list,如果依然找到對應(yīng)的 IMP,則返回該實(shí)現(xiàn),否則提示警告信息,返回 NULL,進(jìn)入下一步處理;
4,如果返回值為 NO,返回 NULL,進(jìn)入下一步處理;
?
四,加入消息轉(zhuǎn)發(fā)
在前文《深入淺出Cocoa之消息》一文中,我演示了一個消息轉(zhuǎn)發(fā)的示例,下面我把動態(tài)方法決議部分去除,把消息轉(zhuǎn)發(fā)部分添加進(jìn)來:
// 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運(yùn)行測試代碼,輸出如下:
? >> Bar() in Foo. ? >> forwardInvocation for selector MissMethod ? >> MissMethod() called in Proxy.如果我把動態(tài)方法決議部分代碼也加入進(jìn)來輸出又是怎樣呢?下面只列出了 Foo 的實(shí)現(xiàn)代碼,其他代碼不變動。
@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注意到了沒,消息轉(zhuǎn)發(fā)沒有進(jìn)行!在前文中說過,消息轉(zhuǎn)發(fā)只有在對象無法正常處理消息時才會調(diào)用,而在這里我在動態(tài)方法決議中為 selector 提供了實(shí)現(xiàn),使得對象可以處理該消息,所以消息轉(zhuǎn)發(fā)不會繼續(xù)了。官方文檔中說:
If you implement?resolveInstanceMethod:?but want particular selectors to actually be forwarded via the forwarding mechanism, you return?NO?for those selectors.
文檔里的說法其實(shí)并不準(zhǔn)確,只有在?resolveInstanceMethod 的實(shí)現(xiàn)中沒有真正為 selector 提供實(shí)現(xiàn),并返回 NO 的情況下才會進(jìn)入消息轉(zhuǎn)發(fā)流程;否則絕不會進(jìn)入消息轉(zhuǎn)發(fā)流程,程序要么調(diào)用正確的動態(tài)方法,要么 crash。這也與前面的源碼不太一致,我猜測在比上面源碼的更高層次的地方,再次查找了 method list,如果提供了實(shí)現(xiàn)就能夠找到該實(shí)現(xiàn)。
下面我把 resolveInstanceMethod 方法中為 selector 添加實(shí)現(xiàn)的那一行屏蔽了,消息轉(zhuǎn)發(fā)就應(yīng)該會進(jìn)行:
//class_addMethod([self class], name, (IMP)dynamicMethodIMP, "v@:");再次編譯運(yùn)行,此時輸出正如前面所推斷的那樣:
??>> 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進(jìn)行了消息轉(zhuǎn)發(fā)!而且編譯器很善意地提示(見前面源碼剖析):哎呀,你不能欺騙我嘛,你說添加了實(shí)現(xiàn)(返回YES),其實(shí)還是沒有呀!然后編譯器就無奈地去看能不能消息轉(zhuǎn)發(fā)了。當(dāng)然如果把返回值修改為 NO 就不會有該警告出現(xiàn),其他的輸出不變。
?
五,總結(jié)
從上面的示例演示可以看出,動態(tài)方法決議是先于消息轉(zhuǎn)發(fā)的。
如果向一個 Objective C 對象對象發(fā)送它無法處理的消息(selector),那么編譯器會按照如下次序進(jìn)行處理:
1,首先看是否為該 selector 提供了動態(tài)方法決議機(jī)制,如果提供了則轉(zhuǎn)到 2;如果沒有提供則轉(zhuǎn)到 3;
2,如果動態(tài)方法決議真正為該 selector 提供了實(shí)現(xiàn),那么就調(diào)用該實(shí)現(xiàn),完成消息發(fā)送流程,消息轉(zhuǎn)發(fā)就不會進(jìn)行了;如果沒有提供,則轉(zhuǎn)到 3;
3,其次看是否為該 selector 提供了消息轉(zhuǎn)發(fā)機(jī)制,如果提供了消息了則進(jìn)行消息轉(zhuǎn)發(fā),此時,無論消息轉(zhuǎn)發(fā)是怎樣實(shí)現(xiàn)的,程序均不會 crash。(因為消息調(diào)用的控制權(quán)完全交給消息轉(zhuǎn)發(fā)機(jī)制處理,即使消息轉(zhuǎn)發(fā)并沒有做任何事情,運(yùn)行也不會有錯誤,編譯器更不會有錯誤提示。);如果沒提供消息轉(zhuǎn)發(fā)機(jī)制,則轉(zhuǎn)到 4;
4,運(yùn)行報錯:無法識別的 selector,程序 crash;
?
六,引用
官方運(yùn)行時源代碼:http://www.opensource.apple.com/source/objc4/objc4-532/runtime/
Objective-C Runtime Programming Guide
深入淺出Cocoa之消息
深入淺出Cocoa之類與對象
轉(zhuǎn)載于:https://www.cnblogs.com/kesalin/archive/2012/11/14/dynamic_method_resolve.html
總結(jié)
以上是生活随笔為你收集整理的[深入浅出Cocoa]之消息(二)-详解动态方法决议(Dynamic Method Resolution)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: android翻页实现原理
- 下一篇: 十三、JSP9大隐视对象中四个作用域的大