OC消息转发机制

我们都知道,在OC的开发的过程中,如果不小心调用了一个不存在的方法,这个时候系统就会报出  unrecognized selector sent to instance …

但是从方法调用到报出错误信息,其实是有一个处理过程的,这个过程就是消息的转发机制。

过程中主要涉及的方法有:

//第一步
+ (BOOL)resolveInstanceMethod:(SEL)sel;//实例方法处理 处理实例方法
+ (BOOL)resolveClassMethod:(SEL)sel;//类方法处理 处理类方法
//第二步
- (id)forwardingTargetForSelector:(SEL)aSelector;
//第三步
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;

下面我们一步一步来看一下

首先我们直接新建一个工程,里面添加一个Student类 在其.h文件中声明一个方法(但是不在其.m文件中实现):

-(void)doSomething;

然后我们在页面控制器ViewController.m文件的的ViewDidLoad方法中写一个Student的实例并调用方法:

Student *stu = [[Student alloc]init];
[stu doSomething];

运行发现报错信息:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Student doSomething]: unrecognized selector sent to instance 0x604000017180'

然后我们做一些处理,在Student.m中写入:

void doSomething(id self, SEL _cmd){
    NSLog(@"我来处理了!");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    
    NSLog(@"resolveInstanceMethod:  %@", NSStringFromSelector(sel));
    if (sel == @selector(doSomething)) {
        class_addMethod([self class], sel, (IMP)doSomething, "V@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

实际上是重写 resolveInstanceMethod 方法,如果输入的方法名为 doSomething 则利用运行时动态添加一个doSomething方法 来处理这个事件 这样实际上就相当于实现了doSomething方法。

运行结果为:我来处理了!这是无实现方法时的第一备用方案

现在我们将上述代码 if 部分注释掉 然后新建一个 Teacher 类,并在其中声明并实现一个 doSomething方法:

#import <Foundation/Foundation.h>

@interface Teacher : NSObject

-(void)doSomething;

@end
-(void)doSomething
{
    NSLog(@"Teacher来处理了!");
}

然后 在我们之前建立的 Student类的.m文件中写入如下代码:

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    NSLog(@"forwardingTargetForSelector:  %@", NSStringFromSelector(aSelector));
    Teacher *teacher= [[Teacher alloc] init];
    if ([teacher respondsToSelector:aSelector])
    {
        return teacher;
    }
    return [super forwardingTargetForSelector: aSelector];
}

这个是重写 forwardingTargetForSelector 方法,找到一个能够执行方法名为 aSelector(这里的值为doSomething) 的方法的对象来代替处理这个事件 这就是将消息转发到了 Teacher 类进行处理 。

运行结果为:Teacher 来处理了!这是无实现方法的第二备用方案

再下来我们把上述代码 if部分 也全部注释地 然后新建一个Parent类 里面声明并实现 doSomething方法:

#import "Parent.h"

@implementation Parent

-(void)doSomething
{
    NSLog(@"Parent来处理了!");
}

@end

然后 在我们之前建立的 Student类的.m文件中写入如下代码:

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    NSLog(@"forwardInvocation: %@", NSStringFromSelector([anInvocation selector]));
    if ([anInvocation selector] == @selector(doSomething))
    {
        Parent *parent = [[Parent alloc] init];
        [anInvocation invokeWithTarget:parent];
    }
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    NSLog(@"method signature for selector: %@", NSStringFromSelector(aSelector));
    if (aSelector == @selector(doSomething))
    {
        return [NSMethodSignature signatureWithObjCTypes:"V@:@"];
    }
    return [super methodSignatureForSelector:aSelector];
}

这里是用 methodSignatureForSlector 方法生成一个方法签名传递到 forwardinvocation 方法中进行使用,在forwardinvocation 方法中通过签名将 消息转发到了 Parent 类进行处理。平时就是到了这一步 因为 methodSignatureForSlector 方法找不到对应的方法,就返回了一个空的签名 所以就会报错。

这里的运行结果为:Parent来处理了! 这是无实现方法的第三备用方案

关于生成签名的类型"v@:"解释一下。每一个方法会默认隐藏两个参数,self、_cmd,self代表方法调用者,_cmd代表这个方法的SEL,签名类型就是用来描述这个方法的返回值、参数的,v代表返回值为void,@表示self,:表示_cmd。

实际上最后消息未能处理 还会调用到方法:

- (void)doesNotRecognizeSelector:(SEL)aSelector;

我们也可以在这里写一些处理 来避免 crash!但是不建议这么做!

由此我们可以看出在调用一个未实现的方法时执行的全过程,也就是这 三步 处理,依次在上一步未处理的情况下走到下一步,最后如果都没有处理就报错!



阅读更多
个人分类: iOS底层机制
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭