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

罗朝辉 (http://www.cnblogs.com/kesalin/)

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

序言

如果我们在 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,如果没有返回 NULLif (!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 selfmeth = 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;}}// 其他情况下返回 NULLreturn 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 return NO 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/

Objective-C Runtime Programming Guide

深入浅出Cocoa之消息

深入浅出Cocoa之类与对象

转载于:https://www.cnblogs.com/kesalin/archive/2012/11/14/dynamic_method_resolve.html

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

  1. MQ消息队列详解、四大MQ的优缺点分析

    MQ消息队列详解.四大MQ的优缺点分析 前言 面试题切入 面试官心理分析 面试题剖析 ①为什么要使用MQ 系统解耦 异步调用 流量削峰 消息队列的优缺点 四大主流MQ(kafka.ActiveMQ.R ...

  2. 窗口发送消息参数详解

    窗口发送消息参数详解 //    窗口.发送消息    函数功能: 将指定的消息发送到一个窗口,同win32 api 里面的SendMessage等同的效果 中文函数原型: 发送消息(hwnd,msg ...

  3. Windows 窗口发送消息参数详解

    窗口发送消息参数详解 // 窗口.发送消息 函数功能: 将指定的消息发送到一个窗口,同win32 api 里面的SendMessage等同的效果中文函数原型: 发送消息(hwnd,msg,wparam ...

  4. LwIP 之六 详解动态内存管理 内存池(memp.c/h)

      该文主要是接上一部分LwIP 之 详解动态内存管理 内存堆(mem.c/h),该部分许多内容需要用到上一篇的内容.该部分主要是详细介绍LwIP中的动态内存池.整个内存池的实现相较于内存堆来说,还是 ...

  5. LwIP 之五 详解动态内存管理 内存堆(mem.c/h)

    写在前面   目前网上有很多介绍LwIP内存的文章,但是绝大多数都不够详细,甚至很多介绍都是错误的!无论是代码的说明还是给出的图例,都欠佳!下面就从源代码,到图例详细进行说明.   目前,网络上多数文 ...

  6. [vb]一些窗口消息的详解

    代码: Public Const WM_NULL = &H0 '空消息 Public Const WM_NCCREATE = &H81 '当窗口第一次被创建时,此消息在WM_CREAT ...

  7. sip消息类型和消息代码详解-转

    在学习asterisk的时候,经常遇到一些远程服务器传回的代码,这些代码都有很重要的信息,让我们了解到对方的sip是如何响应我们这边的sip消息的,于是网上找到了这些sip消息类型和消息代码,自己收藏 ...

  8. wxpython多线程 假死_wxpython多线程防假死与线程间传递消息实例详解

    wxpython中启用线程的方法,将GUI和功能的执行分开. 网上关于python多线程防假死与线程传递消息是几年前的,这里由于wxpython和threading模块已经更新最新,因此给出最新修改代 ...

  9. Mybatis的特性详解——动态SQL

    Mybatis的特性详解--动态SQL 前言 一.动态sql的元素 1.MyBatis if标签:条件判断 2.MyBatis choose.when和otherwise标签 3.MyBatis wh ...

最新文章

  1. DIKW体系(Data-Information-Knowlege-Wisdom)
  2. 210328卡式水分开阶段总结
  3. python 笔记:argparse
  4. 损失函数|交叉熵损失函数
  5. 【亲测好用!】Centos6.5安装Sublime text 3-----可创建桌面图标
  6. javaweb之Filter详解
  7. SAP License:移动类型107和109
  8. 训练史上最佳GAN用了512块TPU,一作自述:这不是算法进步,是算力进步
  9. python3.7 中文汉化包
  10. 物联网技术体系的介绍
  11. 单节锂电池充电管理芯片,IC电路图
  12. 使用爱思助手备份苹果手机数据的方法
  13. php实现邀请好友,新版php获取MSN好友列表,邀请MSN好友
  14. java阶梯计费,机器智能审核阶梯计费方式
  15. 拼音检索VS五笔检索---Javascript实现
  16. 详解Unity中的粒子系统Particle System (九)
  17. Qt QClipboard 粘贴复制功能学习
  18. 使用Graham扫描法获取一个平面点集的凸包
  19. 云南大学 计算机技术 调剂,云南大学调剂信息(全),希望对大家有所帮助!
  20. 【MySQL5.7指南】第三章——教程

热门文章

  1. 文科生的数据分析:亲测有效,真香!!!
  2. ieee文章作者对不齐怎么办呢?
  3. 要买东西,要买好的,提高效率,经常用的
  4. Final Cut Pro做拜年视频的basic lay out!
  5. U3D 动态创建Prefab的多个实例
  6. 基于 Kubernetes 的 FaaS 平台开源实现——Kubeless 的应用实践
  7. 《可爱的Python》读书笔记(五)
  8. 【Maven学习】Nexus OSS私服仓库的备份与迁移
  9. WiFi万能钥匙发布iOS4.0新增骚扰电话拦截功能
  10. 常用JavaScript语法107条