深入浅出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年 [email protected] 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, "[email protected]:");
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, "[email protected]:");

就能在运行期动态地为 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, "[email protected]:");

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

 >> 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, "[email protected]:");
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, "[email protected]:");

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

  >> 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之类与对象

时间: 2024-12-16 02:13:51

深入浅出Cocoa之消息(二)-详解动态方法决议(Dynamic Method Resolution) 【转】的相关文章

多态,动态方法调度(dynamic method dispatch)?

8.多态Polymorphism,向上转型Upcasting,动态方法调度(dynamic method dispatch) (视频下载) (全部书籍) 什么叫多态?简言之,马 克 - t o - w i n:就是父类引用指向子类时,父类和子类必须同时拥有某个同名函数,父类引用到底指向谁(调用谁的函数),是在runtime时决定的,因此呈现多种状态(不知道会指向若干子类中的哪一个还是父类自己).拿上一节的例子来讲,比如运行时如果用户输入自行车,就执行自行车的驾驶方法.如果用户输入小轿车,就执行小

ios底层开发消息机制(三)动态方法决议

序言 如果我们在 Objective C 中向一个对象发送它无法处理的消息,会出现什么情况呢?根据前文<深入浅出Cocoa之消息>的介绍,我们知道发送消息是通过 objc_send(id, SEL, ...) 来实现的,它会首先在对象的类对象的 cache,method list 以及父类对象的 cache, method list 中依次查找 SEL 对应的 IMP:如果没有找到且实现了动态方法决议机制就会进行决议,如果没有实现动态方法决议机制或决议失败且实现了消息转发机制就会进入消息转发流

动态方法决议 和 消息转发

如果我们在 Objective C 中向一个对象发送它无法处理的消息,会出现什么情况呢?我们知道发送消息是通过 objc_send(id, SEL, ...) 来实现的,它会首先在对象的类对象的 cache,method list 以及父类对象的 cache, method list 中依次查找 SEL 对应的 IMP:这个是需要对类对象的结构熟悉,不清楚的可以参考我的下一篇文章<object-c 类结构解析>,如果没有找到且实现了动态方法决议机制就会进行决议,如果没有实现动态方法决议机制或决

深入浅出Cocoa之消息

[Cocoa]深入浅出Cocoa之消息 深入浅出Cocoa之消息    罗朝辉 (http://www.cnblogs.com/kesalin/) 本文遵循“署名-非商业用途-保持一致”创作公用协议 在入门级别的ObjC 教程中,我们常对从C++或Java 或其他面向对象语言转过来的程序员说,ObjC 中的方法调用(ObjC中的术语为消息)跟其他语言中的方法调用差不多,只是形式有些不同而已. 譬如C++ 中的: Bird * aBird = new Bird(); aBird->fly(); 在

[Cocoa]深入浅出 Cocoa 之消息

深入浅出 Cocoa 之消息    罗朝辉(http://blog.csdn.net/kesalin) 转载请注明出处 在入门级别的ObjC 教程中,我们常对从C++或Java 或其它面向对象语言转过来的程序猿说,ObjC 中的方法调用(ObjC中的术语为消息)跟其它语言中的方法调用差点儿相同,仅仅是形式有些不同而已. 譬如C++ 中的: Bird * aBird = new Bird(); aBird->fly(); 在ObjC 中则例如以下: Bird * aBird = [[Bird al

CSDN Android客户端开发(二):详解如何基于Java用Jsoup爬虫HTML数据

本文参考链接详细介绍如何使用Jsoup包抓取HTML数据,是一个纯java工程,并将其打包成jar包.希望了解如何用java语言爬虫网页的可以看下. 杂家前文就又介绍用HTTP访问百度主页得到html的string字符串,但html的文本数据如果不经过处理就是个文本字符串没有任何效果的.所谓的浏览器就是负责将文本的html"翻译"成看到的界面.在前文有介绍,这个csdn的客户端app分首页.业界.移动.研发.程序员.云计算五大类.以业界为例,http://news.csdn.net/ 

以Apache服务器、php语言为例 详解动态网站的访问过程

目前来说,网站页面主要分为静态页面和动态页面,纯静态页面组成的网站现在相对比较少见,大型网站一般使用的是动态网站建站技术,还有一部分网站是静态网页与动态网页共存, 本文以Apache服务器.php语言为例,详解动态网站的访问过程,下面直接切入本文主题. (1)用户端访问服务器端的html文件 S1:通过本机配置好的DNS域名服务器地址寻找DNS服务器,将网站URL中的Web主机域名解析为Web服务器所在的Linux操作系统(Apache通常与Linux操作系统组合使用)中对应的IP地址. S2:

cocoa动态方法决议及消息转发

假设给一个对象发送不能响应的消息,同一时候又没有进行动态方法决议,又没实现消息转发,那么就会引发以下的crash信息 2014-07-30 15:47:54.434 MethodNotFind[1719:403] -[Person setName:]: unrecognized selector sent to instance 0x100121db0 下面是測试的demo 先看看Person类的定义. #import <Foundation/Foundation.h> @class Car;

转:jQuery.lazyload详解使用方法

来源:http://www.cnblogs.com/wenbo/archive/2011/07/15/2107579.html jQuery实现图片延迟加载,不知道是否可以节省带宽呢?有人知道吗? 这究竟只是一个视觉特效还是真的能延迟加载减少服务器的请求呢? <script type="text/javascript" src="<?php bloginfo('template_directory');?>/js/jquery.lazyload.js&quo