Runtime - 动态方法

<code>[receiver message]</code>调用方法时,如果在message方法在receiver对象的类继承体系中没有找到方法,那怎么办?一般情况下,程序在运行时就会Crash掉,抛出 unrecognized selector sent to …类似这样的异常信息。但在抛出异常之前,还有三次机会按以下顺序让你拯救程序。
<ul>
<li>Method Resolution
用所属类的类方法+(BOOL)resolveInstanceMethod:(实例方法)或者+(BOOL)resolveClassMethod:(类方法),在此方法里添加class_addMethod函数。一般用于@dynamic动态属性。(当一个属性声明为@dynamic,就是向编译器保证编译时不用管/get实现,一定会在运行时实现)。
</li>
<li>Fast Forwarding
如果上一步无法响应消息,调用- (id)forwardingTargetForSelector:(SEL)aSelector方法,将消息接受者转发到另一个对象target(不能为self,否则死循环)。
</li>
<li>Normal Forwarding
如果上一步无法响应消息:
调用方法签名- (NSMethodSignature )methodSignatureForSelector:(SEL)aSelector,方法签名目的将函数的参数类型和返回值封装;
如果返回非nil,则创建一个NSInvocation对象利用方法签名和selector封装未被处理的消息,作为参数传递给- (void)forwardInvocation:(NSInvocation )anInvocation。
这一步比较耗时。
</li>
</ul>

如果以上步骤(消息传递和消息转发)还是不能响应消息,则调动doesNotRecognizeSelector:方法,抛出异常。

unrecognized selector sent to instance

(消息转发可以利用转移消息接受对象,实现伪多重继承的效果。)

[receiver message].jpg

Method Resolution

首先Objective-C在运行时调用<code>+ resolveInstanceMethod:</code>或<code>+ resolveClassMethod:</code>方法,让你添加方法的实现。如果你添加方法并返回YES,那系统在运行时就会重新启动一次消息发送的过程。
举一个简单例子,定义一个类Message,它主要定义一个方法sendMessage,下面就是它的设计与实现:

@interface Message : NSObject
- (void)sendMessage:(NSString *)word;
@end
@implementation Message
- (void)sendMessage:(NSString *)word
{
    NSLog(@"normal way : send message = %@", word);
}
@end

如果我在viewDidLoad方法中创建Message对象并调用<code>sendMessage</code>方法:

- (void)viewDidLoad {
    [super viewDidLoad];

    Message *message = [Message new];
    [message sendMessage:@"Sam Lau"];
}

控制台会打印以下信息:

normal way : send message = Sam Lau

但现在我将原来<code>sendMessage</code>方法实现给注释掉,覆盖<code>resolveInstanceMethod</code>方法:

#pragma mark - Method Resolution

/// override resolveInstanceMethod or resolveClassMethod for changing sendMessage method implementation
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(sendMessage:)) {
        class_addMethod([self class], sel, imp_implementationWithBlock(^(id self, NSString *word) {
            NSLog(@"method resolution way : send message = %@", word);
        }), "v@*");
    }

    return YES;
}

控制台就会打印以下信息:

 method resolution way : send message = Sam Lau

注意到上面代码有这样一个字符串"v@*,它表示方法的参数和返回值,详情请参考Type Encodings
如果resolveInstanceMethod方法返回NO,运行时就跳转到下一步:消息转发(Message Forwarding)

Fast Forwarding

如果目标对象实现<code>- forwardingTargetForSelector:</code>方法,系统就会在运行时调用这个方法,只要这个方法返回的不是nil或self,也会重启消息发送的过程,把这消息转发给其他对象来处理。否则,就会继续Normal Fowarding。

继续上面Message类的例子,将<code>sendMessage</code>和<code>resolveInstanceMethod</code>方法注释掉,然后添加<code>forwardingTargetForSelector</code>方法的实现:

#pragma mark - Fast Forwarding
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if (aSelector == @selector(sendMessage:)) {
        return [MessageForwarding new];
    }
    return nil;
}

此时还缺一个转发消息的类<code>MessageForwarding</code>,这个类的设计与实现如下:

@interface MessageForwarding : NSObject
- (void)sendMessage:(NSString *)word;
@end
@implementation MessageForwarding
- (void)sendMessage:(NSString *)word
{
    NSLog(@"fast forwarding way : send message = %@", word);
}
@end

此时,控制台会打印以下信息:

fast forwarding way : send message = Sam Lau

这里叫Fast,是因为这一步不会创建NSInvocation对象,但Normal Forwarding会创建它,所以相对于更快点。

Normal Forwarding

如果没有使用Fast Forwarding来消息转发,最后只有使用Normal Forwarding来进行消息转发。它首先调用<code>methodSignatureForSelector:</code>方法来获取函数的参数和返回值,如果返回为nil,程序会Crash掉,并抛出unrecognized selector sent to instance异常信息。如果返回一个函数签名,系统就会创建一个NSInvocation对象并调用<code>-forwardInvocation:</code>方法。
继续前面的例子,将<code>forwardingTargetForSelector</code>方法注释掉,添加<code>methodSignatureForSelector</code>和<code>forwardInvocation</code>方法的实现:

#pragma mark - Normal Forwarding
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];

    if (!methodSignature) {
        methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:*"];
    }

    return methodSignature;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    MessageForwarding *messageForwarding = [MessageForwarding new];

    if ([messageForwarding respondsToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:messageForwarding];
    }
}

示例代码 github

三种方法的选择

Runtime提供三种方式来将原来的方法实现代替掉,那该怎样选择它们呢?
<ul>
<li>Method Resolution:由于Method Resolution不能像消息转发那样可以交给其他对象来处理,所以只适用于在原来的类中代替掉。</li>
<li>Fast Forwarding:它可以将消息处理转发给其他对象,使用范围更广,不只是限于原来的对象。</li>
<li>Normal Forwarding:它跟Fast Forwarding一样可以消息转发,但它能通过NSInvocation对象获取更多消息发送的信息,例如:target、selector、arguments和返回值等信息。</li>
</ul>

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,814评论 0 9
  • runtime 运行时语言,实现Object-C的C语言库,将OC转换成C进行编译的过渡者。 作为一门动态编程语言...
    夜雨聲煩_阅读 558评论 0 0
  • runtime 和 runloop 作为一个程序员进阶是必须的,也是非常重要的, 在面试过程中是经常会被问到的, ...
    made_China阅读 1,233评论 0 7
  • runtime 和 runloop 作为一个程序员进阶是必须的,也是非常重要的, 在面试过程中是经常会被问到的, ...
    SOI阅读 21,863评论 3 63
  • 朋友说羡慕我们一家三口在一起无话不说,其乐融融的样子;孩子几位好友的妈妈也说你们怎么就能和孩子沟通无障碍,像朋友...
    微笑的花朵阅读 373评论 1 2