iOS-performSelector实现多个参数

2021-08-04  本文已影响0人  凉风起君子意如何

其实在日常iOS开发中,真正使用invocation的地方还是很少,趁着这段时间公司项目空闲期,对之前的面试题做个简单回忆总结。
怎么实现performSelector支持2个以上的参数?
说说对isa指针的理解?
常见的crash?说说unrecognize selector to instance 具体发生过程
以上,其实都涉及到方法具体是如何调用执行的。以下都是对问题一做个简单说明使用

使用一:实现performSelector传入>2参数

OC里常用调用方法的几种方式

// 方式一:中括号形式
[super viewDidLoad];

// 方式二:系统performSelector形式,相关的几个api,但最多只支持2个参数
[obj performSelector:@selector(setName:) withObject:@"bfoOnline"];

// 方式三:NSMethodSignature + NSInvocation形式,没有参数限制
[obj performSelector:@selector(run:str2:str3:str4:) withObjects:arrArgs];

NSMethodSignature+ selector+ NSInvocation简单说明

NSMethodSignature:方法签名中保存了方法的名称/参数/返回值,协同NSInvocation来进行消息的转发。方法签名一般是用来设置参数和获取返回值的, 和方法的调用没有太大的关系。

selector :SEL类型 它简单的唯一标示了一个方法,扮演类似于一个动态函数指针,可以简单理解为一个唯一的id

NSInvocation:它是命令模式的一种实现,它包含选择器、方法签名、相应的参数以及目标对象。当NSInvocation被调用,它会在运行时通过目标对象去寻找对应的方法,从而确保唯一性。

方式三 selector+NSMethodSignature + NSInvocation使用

封装在NSObject分类里的核心代码(具体步骤和使用请看注释)

// 扩展performSelector方法,支持多参数(系统最多只支持2个参数)
- (id)performSelector:(SEL)aSel withObjects:(NSArray*)objects {
    id res = nil;

    // 1 根据sel实例化NSMethodSignature
    NSMethodSignature *signature = [[self class] instanceMethodSignatureForSelector:aSel];
    if (!signature) {
        // 没找到,抛出异常
        @throw [NSException exceptionWithName:@"抛异常错误" reason:@"没有这个方法,或这个方法名字错误" userInfo:nil];
        return res;
    }
    // 2 创建invocation对象
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    // 3 设置调用者
    [invocation setTarget:self];
    // 4 设置selector
    [invocation setSelector:aSel];
    // 5 设置参数
    NSUInteger argsCount = signature.numberOfArguments - 2; // 去掉默认的2个参数:self和_cmd(调用者target和selector)
    NSUInteger arrCount = objects.count;
    // 不能简单的通过遍历参数数组来设置参数,因为外界传进来的参数个数是不可控的,而是取上述两者之间最小值
    NSUInteger resultCount = MIN(argsCount, arrCount);
    for (int i=0; i<resultCount; i++) {
        id obj = objects[i];
        if ([obj isKindOfClass:[NSNull class]]) {
            obj = nil;
        }
        [invocation setArgument:&obj atIndex:i+2];
    }
    // 6 调用
    [invocation invoke];

    // 7 判断当前调用方法是否有返回值
    if (signature.methodReturnLength != 0) {
        [invocation getReturnValue:&res];
    }
    return res;
}

被调用的方法实现(暂时也封装在该分类,在使用过程中 特别是消息转发场景下 注意实现地方)

// 调用的方法实现 (多参数)
- (id)run:(NSString*)str1 str2:(NSString*)str2 str3:(NSString*)str3 str4:(NSString*)str4{
    NSLog(@"%@-----%@-----%@-----%@",str1,str2,str3,str4);
    return nil;
}

vc里简单使用

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSObject *obj = [NSObject new];
    NSArray *arrArgs = @[@"Hello",@"how",@"are",@"you"];
    [obj performSelector:@selector(run:str2:str3:str4:) withObjects:arrArgs];
}

运行结果


fine

注意

参数个数计算问题,注意signature.numberOfArguments - 2,传入的参数需要和其保持一致,只能小于其而不能大于它。


使用二: block+NSInvocation

api声明部分

id invokeBlock(id block,NSArray *args);

@interface NSObject (Test)
@property (nonatomic,copy)NSString *name;
- (id)performSelector:(SEL)aSelector withObjects:(NSArray *)objects;
- (id)run:(NSString*)str1 str2:(NSString*)str2 str3:(NSString*)str3 str4:(NSString*)str4;
@end

核心代码(NSObject+Test.m 全局实现部分)

id invokeBlock(id block,NSArray *args){
    
    if (!block) {
        return  nil;
    }
    id target = [block copy];
    const char *_Block_signature(void*);
    const char *signature = _Block_signature((__bridge void*)target);
    
    NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:signature];
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
    invocation.target = target;
    if ([args isKindOfClass:[NSArray class]]) {
        NSInteger count = MIN(args.count, methodSignature.numberOfArguments-1);
        for (int i=0; i<count; i++) {
            const char *type = [methodSignature getArgumentTypeAtIndex:1+i];
            NSString *typeStr = [NSString stringWithUTF8String:type];
            if ([typeStr containsString:@"\""]) {
                type = [typeStr substringToIndex:1].UTF8String;
            }
            if (strcmp(type, "@")==0) {
                id argument = args[i];
                [invocation setArgument:&argument atIndex:1+i];
            }
            
        }
    }
    [invocation invoke];
    
    id returnVal;
    const char *type = methodSignature.methodReturnType;
    NSString *returnType = [NSString stringWithUTF8String:type];
    if ([returnType containsString:@"\""]) {
        type = [returnType substringToIndex:1].UTF8String;
    }
    if (strcmp(type, "@") == 0) {
        [invocation getReturnValue:&returnVal];
    }
    return returnVal;
    
}

ViewController.m里面调用

- (void)viewDidLoad {
    [super viewDidLoad];
    
//    NSObject *obj = [NSObject new];
//    NSArray *arrArgs = @[@"Hello",@"how",@"are",@"you"];
//    [obj performSelector:@selector(run:str2:str3:str4:) withObjects:arrArgs];
    
//    // 奔溃 :-[CFString release]: message sent to deallocated instance
//    id returnValue = invokeBlock((id)^(NSString *a,NSString *b){return [NSString stringWithFormat:@"%@ and %@",a,b];}, @[@"01",@"bfo"]);
    self.returnValue = invokeBlock((id)^(NSString *a,NSString *b){return [NSString stringWithFormat:@"%@ and %@",a,b];}, @[@"01",@"bfo"]);
    NSLog(@"%@",self.returnValue);
}

注意

注意上面上面注释的局部变量returnValue,若打开注释,执行程序会奔溃,显示EXC_BAD_ACCESS内存访问错误。打开edit scheme里面zombie objects选项(如下图设置),再次运行 console里面会打印-[CFString release]: message sent to deallocated 相关错误,即访问了已被释放的对象


打开zombie objects选项

解决:改成如上内存策略属性,运行正常。
可能原因:自定义的c语言函数,没有被自动释放池包裹,当函数返回值时,就已经被释放。改成copy 属性,引用的时候copy一份,即使之前那个被释放,也不会影响。

后续:待源码解读支撑~

上一篇下一篇

猜你喜欢

热点阅读