序言

如果我们在 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 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之类与对象

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

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

    8.多态Polymorphism,向上转型Upcasting,动态方法调度(dynamic method dispatch) 什么叫多态?简言之,马 克 - t o - w i n:就是父类引用指向子 ...

  2. [Cocoa]深入浅出 Cocoa 之消息

    深入浅出 Cocoa 之消息    罗朝辉(http://blog.csdn.net/kesalin) 转载请注明出处 在入门级别的ObjC 教程中,我们常对从C++或Java 或其它面向对象语言转过 ...

  3. mybatis 详解------动态SQL

    mybatis 详解------动态SQL   目录 1.动态SQL:if 语句 2.动态SQL:if+where 语句 3.动态SQL:if+set 语句 4.动态SQL:choose(when,o ...

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

    假设给一个对象发送不能响应的消息,同一时候又没有进行动态方法决议,又没实现消息转发,那么就会引发以下的crash信息 2014-07-30 15:47:54.434 MethodNotFind[171 ...

  5. [一]class 文件浅析 .class文件格式详解 字段方法属性常量池字段 class文件属性表 数据类型 数据结构

    前言概述  本文旨在讲解class文件的整体结构信息,阅读本文后应该可以完整的了解class文件的格式以及各个部分的逻辑组成含义   class文件包含了java虚拟机指令集 和  符号表   以及若 ...

  6. 详解Vue 方法与事件处理器

      本篇文章主要介绍了详解Vue 方法与事件处理器 ,小编觉得挺不错的,现在分享给大家,也给大家做个参考.一起跟随小编过来看看吧 方法与事件处理器 方法处理器 可以用 v-on 指令监听 DOM 事件 ...

  7. HTTP消息头详解

    HTTP是一个属于应用层面的面向对象的协议,由于其便捷.快速的方式.适用于分布式超媒体信息系统.于1990年提出 HTTP 协议主要特点概括如下 1.支持客户/服务器模式. 2.简单快速 请求方法常用 ...

  8. Android开发——Android的消息机制详解

    )子线程默认是没有Looper的,Handler创建前,必须手动创建,否则会报错.通过Looper.prepare()即可为当前线程创建一个Looper,并通过Looper.loop()来开启消息循环 ...

  9. 深入浅出Mybatis系列四-配置详解之typeAliases别名(mybatis源码篇)

    注:本文转载自南轲梦 注:博主 Chloneda:个人博客 | 博客园 | Github | Gitee | 知乎 上篇文章<深入浅出Mybatis系列(三)---配置详解之properties ...

随机推荐

  1. pip更新升级后Import Error:cannot import name main及pip安装包后出现环境错误拒绝访问

    1. sudo gedit /usr/bin/pip 将pip改为pip._internal 2.pip install XX 改为 pip install --user XX

  2. LL(1),LR(0),SLR(1),LALR(1),LR(1)对比与分析

    前言:考虑到这几种文法如果把具体内容讲下来肯定篇幅太长,而且繁多的符号对初学者肯定是极不友好的,而且我相信看这篇博客的人已经对这几个文法已经有所了解了,本篇博客的内容只是对 这几个文法做一下对比,加深 ...

  3. git 命令行(一)-版本回退

    1. 版本回退 在实际工作中,我们脑子里怎么可能记得一个几千行的文件每次都改了什么内容,不然要版本控制系统干什么.版本控制系统肯定有某个命令可以告诉我们历史记录,在Git中,我们用 git log 命 ...

  4. nginx 访问ssl 的 pem 遇到的权限问题

    nginx 开启失败,日志记录 访问 ssl 的 pem 文件  fopen:Permission denied 权限问题,查看文件权限,目录权限,正常. google 后得知原来是一个 SELinu ...

  5. Windows下shell神器

    想找一个可以在Windows平台玩命令行的东西,不想装虚拟机搞linux,所以找到两个神器 如何升级Babun中的Git Babun中默认已经集成Git,只是有可能不是最新的版本 如果只是更新Babu ...

  6. 1.关于Spring Cloud的一些基本知识

    GA代表 general avaliable 通用可用版  也就是 正式发行版 PRE 代表预版本  就是还没有成熟 SNAPSHOT 快照版 这个版本可用 没有bug但是后期还会改进 选了这个spr ...

  7. 2019.10.26 csp-s模拟测试88 反思总结

    今天的主人公是什么? 60.1K!!!! 先扔代码再更新防止我等会儿一上头不打算写完题解 T1: #include<iostream> #include<cstdio> #in ...

  8. Newtonsoft.json 二次引用出错解决办法

    一.一般在C# 项目中二次引用会出现如下错误: 解决办法:用编辑器打开项目下的文件(*.csproj),可以找到在这个文件中,Newtonsoft.Json的引用,删掉引用,然后在项目中重新引用就可以 ...

  9. JavaScript内容回顾

    <!DOCTYPE html> <!--JavaScript内容回顾--> <html lang="en"> <head> < ...

  10. php5 常量

    <?php define("GREETING", "Welcome to w3cschool.cn!", true); echo greeting; ?& ...