[深入浅出Cocoa]之消息(二)-详解动态方法决议(Dynamic Method Resolution)


 [深入浅出Cocoa]之消息(二)-详解动态方法决议(Dynamic Method Resolution)

罗朝辉 (http://blog.csdn.net/kesalin/)

本文遵循“ 署名-非商业用途-保持一致”创作公用协议
 

序言

如果我们在 Objective C 中向一个对象发送它无法处理的消息,会出现什么情况呢?根据前文《深入浅出Cocoa之消息》的介绍,我们知道发送消息是通过 objc_send(id, SEL, ...) 来实现的,它会首先在对象的类对象的 cache,method list 以及父类对象的 cache, method list 中依次查找 SEL 对应的 IMP;如果没有找到且实现了动态方法决议机制就会进行决议,如果没有实现动态方法决议机制或决议失败且实现了消息转发机制就会进入消息转发流程,否则程序 crash。也就是说如果同时提供了动态方法决议和消息转发,那么动态方法决议先于消息转发,只有当动态方法决议依然无法正确决议 selector 的实现,才会尝试进行消息转发。在前文中,我并没有详细讲解动态方法决议,因此本文将详细介绍之。

本文代码下载:点此下载

一,向一个对象发送该对象无法处理的消息

如下代码:

  1. @interface Foo : NSObject  
  2.   
  3. -(void)Bar;  
  4.   
  5. @end  
  6.   
  7. @implementation Foo  
  8.   
  9. -(void)Bar  
  10. {  
  11.     NSLog(@" >> Bar() in Foo");  
  12. }  
  13.   
  14. @end  
  15.   
  16. /  
  17. #import "Foo.h"  
  18.   
  19. int main (int argc, const char * argv[])  
  20. {  
  21.   
  22.     @autoreleasepool {  
  23.           
  24.         Foo * foo = [[Foo alloc] init];  
  25.           
  26.         [foo Bar];  
  27.           
  28.         [foo MissMethod];  
  29.           
  30.         [foo release];  
  31.     }  
  32.     return 0;  
  33. }  

在编译时,XCode 会提示警告:

Instance method '-MissMethod' not found (return type defaults to 'id')

如果,我们忽视该警告运行之,一定会 crash:

  1. >> Bar() in Foo  
  2. -[Foo MissMethod]: unrecognized selector sent to instance 0x10010c840  
  3. *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Foo MissMethod]: unrecognized selector sent to instance 0x10010c840'  
  4. *** Call stack at first throw:  
  5. ......  
  6. terminate called after throwing an instance of 'NSException'  

下划线部分就是造成 crash 的原因:对象无法处理 MissMethod 对应的 selector,也就是没有相应的实现。

 

二,动态方法决议

Objective C 提供了一种名为动态方法决议的手段,使得我们可以在运行时动态地为一个 selector 提供实现。我们只要实现 +resolveInstanceMethod: 和/或 +resolveClassMethod: 方法,并在其中为指定的 selector  提供实现即可(通过调用运行时函数 class_addMethod 来添加)。这两个方法都是 NSObject 中的类方法,其原型为:

  1. + (BOOL)resolveClassMethod:(SEL)name;  
  2. + (BOOL)resolveInstanceMethod:(SEL)name;  

参数 name 是需要被动态决议的 selector;返回值文档中说是表示动态决议成功与否。但在上面的例子中(不涉及消息转发的情况下),如果在该函数内为指定的 selector  提供实现,无论返回 YES 还是 NO,编译运行都是正确的;但如果在该函数内并不真正为 selector 提供实现,无论返回 YES 还是 NO,运行都会 crash,道理很简单,selector 并没有对应的实现,而又没有实现消息转发。resolveInstanceMethod 是为对象方法进行决议,而 resolveClassMethod 是为类方法进行决议。

下面我们用动态方法决议手段来修改上面的代码:

  1. //  
  2. //  Foo.m  
  3. //  DeepIntoMethod  
  4. //  
  5. //  Created by 飘飘白云 on 12-11-13.  
  6. //  Copyright (c) 2012年 kesalin@gmail.com All rights reserved.  
  7. //  
  8.   
  9. #import "Foo.h"  
  10. #include <objc/runtime.h>  
  11.   
  12. void dynamicMethodIMP(id self, SEL _cmd) {  
  13.     NSLog(@" >> dynamicMethodIMP");  
  14. }  
  15.   
  16. @implementation Foo  
  17.   
  18. -(void)Bar  
  19. {  
  20.     NSLog(@" >> Bar() in Foo");  
  21. }  
  22.   
  23. + (BOOL)resolveInstanceMethod:(SEL)name  
  24. {     
  25.     NSLog(@" >> Instance resolving %@", NSStringFromSelector(name));  
  26.       
  27.     if (name == @selector(MissMethod)) {  
  28.         class_addMethod([self class], name, (IMP)dynamicMethodIMP, "v@:");  
  29.         return YES;  
  30.     }  
  31.       
  32.     return [super resolveInstanceMethod:name];  
  33. }  
  34.   
  35. + (BOOL)resolveClassMethod:(SEL)name  
  36. {  
  37.     NSLog(@" >> Class resolving %@", NSStringFromSelector(name));  
  38.       
  39.     return [super resolveClassMethod:name];  
  40. }  
  41.   
  42. @end  

在前文《深入浅出Cocoa之消息》中已经介绍过 Objective C 中的方法其实就是至少带有两个参数(self 和 _cmd)的普通 C 函数,因此在上面的代码中提供这样一个 C 函数 dynamicMethodIMP,让它来充当对象方法 MissMethod 这个 selector 的动态实现。因为 MissMethod 是被对象所调用,所以它被认为是一个对象方法,因而应该在 resolveInstanceMethod 方法中为其提供实现。通过调用

[objc]  view plain copy print ?
  1. class_addMethod([self class], name, (IMP)dynamicMethodIMP, "v@:");  

就能在运行期动态地为 name 这个 selector 添加实现:dynamicMethodIMP。class_addMethod 是运行时函数,所以需要导入头文件:objc/runtime.h。

再次编译运行前面的测试代码,输出如下:

  1.   >> Bar() in Foo.  
  2.   >> Instance resolving MissMethod  
  3.   >> dynamicMethodIMP called.  
  4.   >> Instance resolving _doZombieMe  

dynamicMethodIMP 被调用了,crash 没有了!万事大吉!

注意:这里两次调用了 resolveInstanceMethod,而且两次决议的 selector 在不同的系统下是不同的,上面演示的是 10.7 系统下第一个决议 MissMethod,第二个决议 _doZombieMe;在 10.6 系统下两次都是决议 MissMethod。

下面我把 resolveInstanceMethod 方法中为 selector 添加实现的那一行屏蔽了,消息转发就应该会进行:

//class_addMethod([self class], name, (IMP)dynamicMethodIMP, "v@:");

再次编译运行,此时输出:

  1.  >> Bar() in Foo.  
  2.  >> Instance resolving MissMethod  
  3.  +[Foo resolveInstanceMethod:MissMethod] returned YES, but no new implementation of -[Foo MissMethod] was found  
  4.   >> Instance resolving _doZombieMe  
  5.  objc[1223]: +[Foo resolveInstanceMethod:MissMethod] returned YES, but no new implementation of -[Foo MissMethod] was found  
  6.  -[Foo MissMethod]: unrecognized selector sent to instance 0x10010c880  
  7.   *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Foo MissMethod]: unrecognized selector sent to instance 0x10010c880'  
  8.  *** Call stack at first throw:  
  9.  ......  

在这里,resolveInstanceMethod 使诈了,它声称成功(返回 YES )决议了 selector,但是并没有真正提供实现,被编译器发觉而提示相应的错误信息。那它的返回值到底有什么作用呢,在它没有提供真正的实现,并且提供了消息转发机制的情况下,YES 表示不进行后续的消息转发,返回  NO 则表示要进行后续的消息转发。

 

三,源码剖析

让我们来看看运行时系统是如何进行动态方法决议的,下面的代码来自苹果官方公开的源码 objc-class.mm,我在其中添加了中文注释:

1,首先是判断是不是要进行类方法决议,如果不是或决议失败,则进行实例方法决议(请参考:《深入浅出Cocoa之类与对象》):

  1. /*********************************************************************** 
  2. * _class_resolveMethod 
  3. * Call +resolveClassMethod or +resolveInstanceMethod and return  
  4. * the method added or NULL.  
  5. * Assumes the method doesn't exist already. 
  6. **********************************************************************/  
  7. __private_extern__ Method _class_resolveMethod(Class cls, SEL sel)  
  8. {  
  9.     Method meth = NULL;  
  10.   
  11.     if (_class_isMetaClass(cls)) {  
  12.         meth = _class_resolveClassMethod(cls, sel);  
  13.     }  
  14.     if (!meth) {  
  15.         meth = _class_resolveInstanceMethod(cls, sel);  
  16.     }  
  17.   
  18.     if (PrintResolving  &&  meth) {  
  19.         _objc_inform("RESOLVE: method %c[%s %s] dynamically resolved to %p",   
  20.                      class_isMetaClass(cls) ? '+' : '-',   
  21.                      class_getName(cls), sel_getName(sel),   
  22.                      method_getImplementation(meth));  
  23.     }  
  24.       
  25.     return meth;  
  26. }  

2,类方法决议与实例方法决议大体相似,在这里就只看实例方法决议部分了:

  1. /*********************************************************************** 
  2.  * _class_resolveInstanceMethod 
  3.  * Call +resolveInstanceMethod and return the method added or NULL. 
  4.  * cls should be a non-meta class. 
  5.  * Assumes the method doesn't exist already. 
  6.  **********************************************************************/  
  7. static Method _class_resolveInstanceMethod(Class cls, SEL sel)  
  8. {  
  9.     BOOL resolved;  
  10.     Method meth = NULL;  
  11.       
  12.     // 是否实现了 resolveInstanceMethod,如果没有返回 NULL  
  13.     if (!look_up_method(((id)cls)->isa, SEL_resolveInstanceMethod,   
  14.                         YES /*cache*/, NO /*resolver*/))  
  15.     {  
  16.         return NULL;  
  17.     }  
  18.       
  19.     // 调用 resolveInstanceMethod,并获取返回值  
  20.     resolved = ((BOOL(*)(id, SEL, SEL))objc_msgSend)((id)cls, SEL_resolveInstanceMethod, sel);  
  21.       
  22.     if (resolved) {  
  23.         // 返回值为 YES,表示 resolveInstanceMethod 声称它已经成功添加实现,则再次查找 method list   
  24.         // +resolveClassMethod adds to self  
  25.         meth = look_up_method(cls, sel, YES/*cache*/, NO/*resolver*/);  
  26.           
  27.         if (!meth) {  
  28.             // resolveInstanceMethod 使诈了,它声称成功添加实现了,但实际没有,给出警告信息,并返回 NULL  
  29.             // Method resolver didn't add anything?  
  30.             _objc_inform("+[%s resolveInstanceMethod:%s] returned YES, but "  
  31.                          "no new implementation of %c[%s %s] was found",   
  32.                          class_getName(cls),  
  33.                          sel_getName(sel),   
  34.                          class_isMetaClass(cls) ? '+' : '-',   
  35.                          class_getName(cls),   
  36.                          sel_getName(sel));  
  37.             return NULL;  
  38.         }  
  39.     }  
  40.       
  41.     // 其他情况下返回 NULL  
  42.     return meth;  
  43. }  

这段代码很容易理解:

1,首先判断是否实现了 resolveInstanceMethod,如果没有实现,返回 NULL,进入下一步处理;

2,如果实现了,调用 resolveInstanceMethod,获取返回值;

3,如果返回值为 YES,表示 resolveInstanceMethod 声称它已经提供了 selector 的实现,因此再次查找 method list,如果依然找到对应的 IMP,则返回该实现,否则提示警告信息,返回 NULL,进入下一步处理;

4,如果返回值为 NO,返回 NULL,进入下一步处理;

 

四,加入消息转发

在前文《深入浅出Cocoa之消息》一文中,我演示了一个消息转发的示例,下面我把动态方法决议部分去除,把消息转发部分添加进来:

  1. // Proxy  
  2. @interface Proxy : NSObject  
  3.   
  4. -(void)MissMethod;  
  5.   
  6. @end  
  7.   
  8. @implementation Proxy  
  9.   
  10. -(void)MissMethod  
  11. {  
  12.     NSLog(@" >> MissMethod() called in Proxy.");  
  13. }  
  14.   
  15. @end  
  16.   
  17. // Foo  
  18. @interface Foo : NSObject  
  19.   
  20. -(void)Bar;  
  21.   
  22. @end  
  23.   
  24. @implementation Foo  
  25.   
  26. - (void)forwardInvocation:(NSInvocation *)anInvocation  
  27. {  
  28.     SEL name = [anInvocation selector];  
  29.     NSLog(@" >> forwardInvocation for selector %@", NSStringFromSelector(name));  
  30.       
  31.     Proxy * proxy = [[[Proxy alloc] init] autorelease];  
  32.     if ([proxy respondsToSelector:name]) {  
  33.         [anInvocation invokeWithTarget:proxy];  
  34.     }  
  35.     else {  
  36.         [super forwardInvocation:anInvocation];  
  37.     }  
  38. }  
  39.   
  40. - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {  
  41.     return [Proxy instanceMethodSignatureForSelector:aSelector];  
  42. }  
  43.   
  44. -(void)Bar  
  45. {  
  46.     NSLog(@" >> Bar() in Foo.");  
  47. }  
  48.   
  49. @end  

运行测试代码,输出如下:

  1. >> Bar() in Foo.  
  2.  >> forwardInvocation for selector MissMethod  
  3.  >> MissMethod() called in Proxy.  

如果我把动态方法决议部分代码也加入进来输出又是怎样呢?下面只列出了 Foo 的实现代码,其他代码不变动。

  1. @implementation Foo  
  2.   
  3. +(BOOL)resolveInstanceMethod:(SEL)name  
  4. {  
  5.     NSLog(@" >> Instance resolving %@", NSStringFromSelector(name));  
  6.       
  7.     if (name == @selector(MissMethod)) {  
  8.         class_addMethod([self class], name, (IMP)dynamicMethodIMP, "v@:");  
  9.         return YES;  
  10.     }  
  11.       
  12.     return [super resolveInstanceMethod:name];  
  13. }  
  14.   
  15. +(BOOL)resolveClassMethod:(SEL)name  
  16. {  
  17.     NSLog(@" >> Class resolving %@", NSStringFromSelector(name));  
  18.     return [super resolveClassMethod:name];  
  19. }  
  20.   
  21. - (void)forwardInvocation:(NSInvocation *)anInvocation  
  22. {  
  23.     SEL name = [anInvocation selector];  
  24.     NSLog(@" >> forwardInvocation for selector %@", NSStringFromSelector(name));  
  25.       
  26.     Proxy * proxy = [[[Proxy alloc] init] autorelease];  
  27.     if ([proxy respondsToSelector:name]) {  
  28.         [anInvocation invokeWithTarget:proxy];  
  29.     }  
  30.     else {  
  31.         [super forwardInvocation:anInvocation];  
  32.     }  
  33. }  
  34.   
  35. - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {  
  36.     return [Proxy instanceMethodSignatureForSelector:aSelector];  
  37. }  
  38.   
  39. -(void)Bar  
  40. {  
  41.     NSLog(@" >> Bar() in Foo.");  
  42. }  
  43.   
  44. @end  

此时,输出为:

  1. >> Bar() in Foo.  
  2. >> Instance resolving MissMethod  
  3. >> dynamicMethodIMP called.  
  4. >> Instance resolving _doZombieMe  

注意到了没,消息转发没有进行!在前文中说过,消息转发只有在对象无法正常处理消息时才会调用,而在这里我在动态方法决议中为 selector 提供了实现,使得对象可以处理该消息,所以消息转发不会继续了。官方文档中说:

If you implement resolveInstanceMethod: but want particular selectors to actually be forwarded via the forwarding mechanism, you return NO for those selectors.

文档里的说法其实并不准确,只有在 resolveInstanceMethod 的实现中没有真正为 selector 提供实现,并返回 NO 的情况下才会进入消息转发流程;否则绝不会进入消息转发流程,程序要么调用正确的动态方法,要么 crash。这也与前面的源码不太一致,我猜测在比上面源码的更高层次的地方,再次查找了 method list,如果提供了实现就能够找到该实现。

下面我把 resolveInstanceMethod 方法中为 selector 添加实现的那一行屏蔽了,消息转发就应该会进行:

//class_addMethod([self class], name, (IMP)dynamicMethodIMP, "v@:");

再次编译运行,此时输出正如前面所推断的那样:

  1. >> Bar() in Foo.  
  2. >> Instance resolving MissMethod  
  3. objc[1618]: +[Foo resolveInstanceMethod:MissMethod] returned YES, but no new implementation of -[Foo MissMethod] was found  
  4. >> forwardInvocation for selector MissMethod  
  5. >> MissMethod() called in Proxy.  
  6. >> 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/

Objective-C Runtime Programming Guide

深入浅出Cocoa之消息

深入浅出Cocoa之类与对象

----------------------------------------------------------

ps:本人是结合这位大神的文章来理解

http://blog.csdn.net/yiyaaixuexi/article/details/8970734


ps:

1.要实现动态方法决议则要实现以下方法:

+ (BOOL)resolveInstanceMethod:(SEL)name

+ (BOOL)resolveClassMethod:(SEL)name

2.要实现消息转发则要实现以下两个方法:

- (void)forwardInvocation:(NSInvocation *)anInvocation

- (NSMethodSignature *) methodSignatureForSelector:(SEL)aSelector

  注:runtime会发送(即先执行)methodSignatureForSelector:消息获取Selector对应的方法签名。返回值非空,则调用forwardInvocation该方法,返回值为空,则向当前对象发送doesNotRecognizeSelector:消息,程序崩溃退出。



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
eclipse-jee-2023-03-r-macosx-cocoa-x86_64.dmg 是一个针对 macOS 系统的 Eclipse JEE 版本的安装文件。Eclipse 是一款开源的、跨平台的集成开发环境,为软件开发者提供了丰富的工具和功能,特别是针对 JavaEE 开发。 这个安装文件的版本号是 2023-03-r,意味着它是在2023年3月发布的第一个有重要改进的版本。macOSx-cocoa-x86_64 则表示这个程序适用于 macOS 操作系统的 cocoa 架构和 x86_64 位的处理器。.dmg 是 macOS 系统上常用的磁盘映像文件的扩展名。 通过安装这个文件,你可以在你的 macOS 系统上使用 Eclipse JEE 版本进行 JavaEE 的开发工作。它提供了一系列的插件和工具,包括代码编辑器、调试器、自动补全、版本控制等,以支持 JavaEE 技术的开发。你可以轻松地创建、编译、运行和调试 JavaEE 项目,并且可以方便地导入和管理项目。 Eclipse JEE 版本还针对 JavaEE 开发提供了额外的功能和集成,比如支持服务器的管理和部署、Web 服务的开发和测试、数据源的管理等。你可以通过插件来进一步扩展它的功能,以满足你对开发环境的特殊需求。 总而言之,eclipse-jee-2023-03-r-macosx-cocoa-x86_64.dmg 是一款适用于 macOS 系统的 Eclipse JEE 开发环境的安装文件。安装后,你可以利用它进行 JavaEE 技术的开发,包括创建、编辑、运行和调试项目,并享受一系列丰富的功能和插件。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值