NSMethodSignature与NSInvocation

NSMethodSignature与NSInvocation简单使用

如果我们只知道一个方法的名称@"test:",那如何调用这个方法呢?
我们的第一反应肯定是会想到通过 performSelector: withObject: 调用:

    SEL selector = NSSelectorFromString(@"test:");
    [instance performSelector:selector withObject:object];

这一点问题也没有,但如果这个test方法是多参数的或者有返回值的,这时performSelector肯定是行不通的。这时我们就需要用到NSMethodSignature与NSInvocation了,直接贴代码:

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self invocationMethod];
}

- (void)invocationMethod {
    // method1
    SEL selector1 = @selector(method1);
    NSMethodSignature *signature1 = [[self class] instanceMethodSignatureForSelector:selector1]; // selector ====> signature
    NSInvocation *invocation1 = [NSInvocation invocationWithMethodSignature:signature1]; // signature ====> invocation
    invocation1.target = self;
    invocation1.selector = selector1;
    [invocation1 invoke];
    
    // method2
    SEL selector2 = @selector(method2);
    NSMethodSignature *signature2 = [[self class] instanceMethodSignatureForSelector:selector2];
    NSInvocation *invocation2 = [NSInvocation invocationWithMethodSignature:signature2];
    invocation2.target = self;
    invocation2.selector = selector2;
    [invocation2 invoke];
    NSString *value2 = @"";
    [invocation2 getReturnValue:&value2];
    NSLog(@"%@",value2);
    
    // method3
    SEL selector3 = @selector(method3:argument2:);
    NSMethodSignature *signature3 = [[self class] instanceMethodSignatureForSelector:selector3];
    NSInvocation *invocation3 = [NSInvocation invocationWithMethodSignature:signature3];
    invocation3.target = self;
    invocation3.selector = selector3;
    NSString *argument1 = @"argument1";
    NSString *argument2 = @"argument2";
    [invocation3 setArgument:&argument1 atIndex:2]; // 方法实际上有两个隐藏参数:self和_cmd 这里参数的index为2
    [invocation3 setArgument:&argument2 atIndex:3];
    [invocation3 invoke];
}

- (void)method1 {
    NSLog(@"method1");
}

- (NSString *)method2 {
    NSLog(@"method2");
    return @"method2 return";
}

- (void)method3:(NSString*)argument1 argument2:(NSString *)argument2 {
    NSLog(@"method3");
    NSLog(@"%@,%@",argument1,argument2);
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

实现起来会有点繁琐,但却很实用,运行的结果:

2018-02-09 15:53:17.591 [4807:606671] method1
2018-02-09 15:53:17.592 [4807:606671] method2
2018-02-09 15:53:17.592 [4807:606671] method2 return
2018-02-09 15:53:17.592 [4807:606671] method3
2018-02-09 15:53:17.592 [4807:606671] argument1,argument2

NSMethodSignature与NSInvocation应用场景

消息转发

  • 将方法交由另一个类实现
#import "ViewController.h"

@interface ViewController ()

@property (nonatomic, strong) UILabel *label;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    _label = [[UILabel alloc] initWithFrame:CGRectMake(100, 100, 200, 100)];
    [self.view addSubview:_label];
    [self performSelector:@selector(setText:) withObject:@"viewController"];
}

// 重载methodSignatureForSelector:得到一个方法aSelector的签名,再由后面的forwardInvocation:去执行
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
    if (!signature) {
        signature = [_label methodSignatureForSelector:aSelector];
    }
    return signature;
}

// forwardInvocation:执行从methodSignatureForSelector:返回的NSMethodSignature,并将NSInvocation转发到其他对象
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL selector = [anInvocation selector];
    if ([_label respondsToSelector:selector]) {
        [anInvocation invokeWithTarget:_label];
    }
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}


@end

(UIViewController包含了UIlabel 属性 label, 如果UIViewController 实例调用setText:方法,由于类没有setText:方法,将会转发给 label 实现)


NSMethodSignature与NSInvocation_第1张图片
UILabel文字设置成功
  • 向NSNull发送消息时导致崩溃问题解决
    对服务器返回的JSON数据解析时总会出现null数据导致崩溃的现象,避免这种崩溃出现的方法之一就是对返回的数据类型进行判断,但每个返回数据的地方都这么判断的话就会有点繁琐。另一个简单实用的方法就是消息转发:将向NSNull发送的消息转发给可以执行该方法的类。这里通过创建一个NSNull的分类就能实现:
#import "NSNull+signature.h"

#define nullObjects @[@"",@0,@[],@{}] // 执行相关方法的数据类型

@implementation NSNull (signature)

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
    if (!signature) {
        for (NSObject *object in nullObjects) {
            signature = [object methodSignatureForSelector:aSelector];
            if (signature) {
                break;
            }
        }
    }
    return signature;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL selector = [anInvocation selector];
    for (NSObject *object in nullObjects) {
        if ([object respondsToSelector:selector]) {
            [anInvocation invokeWithTarget:object];
        }
    }
    
//    [self doesNotRecognizeSelector:selector]; // 抛出异常
}

@end
#import "ViewController.h"
#import "NSNull+signature.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    id nullObject = [NSNull null];
    NSString *nullStr = nullObject;
    NSUInteger strLength = nullStr.length;
    NSLog(@"%lu",(unsigned long)strLength);
}


- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

不加分类运行后会抛出以下异常:

-[NSNull length]: unrecognized selector sent to instance 0x195c466e0

加分类后一切正常,输出结果:

2018-02-11 15:41:01.528 [7186:716497] 0

通过消息转发我们很容易就解决了NSNull崩溃的问题,再也不用担心解析服务器数据为null的情况了。

对于消息转发这里只做了简单的应用,想要了解关于消息转发的更多信息可以参考以下两篇博客:
函数调用
Runtime 你为何如此之屌?

你可能感兴趣的:(NSMethodSignature与NSInvocation)