欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

OC消息转发机制

程序员文章站 2022-07-14 17:43:07
...

我们都知道,在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, "[email protected]:");
        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:"[email protected]:@"];
    }
    return [super methodSignatureForSelector:aSelector];
}

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

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

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

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

- (void)doesNotRecognizeSelector:(SEL)aSelector;

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

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