performSelector 方法的自动俘获特性

局部变量自动俘获

偶然在调试中发现,performSelector 方法具有自动俘获变量的特性。试看如下代码:

CGFloat c = _addViewShowing ? 0 : 80;
    if([self respondsToSelector:@selector(jsq_setToolbarBottomLayoutGuideConstant:)]){
        [self performSelector:@selector(jsq_setToolbarBottomLayoutGuideConstant:) withObject:nil];
        ...
    }

这里请注意 [self performSelector:@selector(jsq_setToolbarBottomLayoutGuideConstant:) withObject:nil]; 一句。在调用 performSelector 方法时,会自动把变量 CGFloat c 俘获到 jsq_setToolbarBottomLayoutGuideConstant: 方法调用中去。也就是说,相当于向该方法传递了参数 c。

值得注意的是,变量 c 的类型必须和 jsq_setToolbarBottomLayoutGuideConstant: 方法参数的类型相同,否则不会自动俘获。例如, c 变量为 CGFloat,而方法jsq_setToolbarBottomLayoutGuideConstant: 的参数同样也为 CGFloat:

- (void)jsq_setToolbarBottomLayoutGuideConstant:(CGFloat)constant

如果你将 c 的类型改成 int,则 c 不会被自动俘获。

利用这个特性,我们可以在 performSelector 调用时,自动传递变量给目标方法,而不用通过 withObject 来传参。

自动俘获方法参数

如果我们将上述代码定义为一个方法:

-(void)setToolbarSpaceToBottom:(CGFloat)constant{
    // 调用私有方法 jsq_setToolbarBottomLayoutGuideConstant
    CGFloat c = _addViewShowing ? 0 : 80;
    if([self respondsToSelector:@selector(jsq_setToolbarBottomLayoutGuideConstant:)]){
        [self performSelector:@selector(jsq_setToolbarBottomLayoutGuideConstant:) withObject:nil];
    }
}

则变量 c 可以省略,因为 performSelector 会自动俘获方法参数 constant,将之传递给 jsq_setToolbarBottomLayoutGuideConstant: 调用。于是这个方法可以写成:

-(void)setToolbarSpaceToBottom:(CGFloat)constant{
    if([self respondsToSelector:@selector(jsq_setToolbarBottomLayoutGuideConstant:)]){
        [self performSelector:@selector(jsq_setToolbarBottomLayoutGuideConstant:) withObject:nil];
    }
}

自动俘获的代价

上述代码同时会带来一个负面作用,即在两个 @selector 引用的地方出现两个相同编译警告:

Undeclared selector ‘jsq_setToolbarBottomLayoutGuideConstant:’

这是因为 jsq_setToolbarBottomLayoutGuideConstant: 方法来自于父类,它是私有的(没有将方法进行静态声明——即未在头文件中声明)。对一切未静态声明的方法进行 performSelector 时,编译器都会提示 Undeclared selector。

你可以用下面的技术消除它们,但这会导致自动俘获失效。

不能使用自动俘获的情况

要消灭编译警告,我们可以使用 NSSelectorFromString 来引用 selector。
例如:

CGFloat c = constant;
    SEL sel=NSSelectorFromString(@"jsq_setToolbarBottomLayoutGuideConstant:");
    if([self respondsToSelector:sel]){
        // 忽略编译器警告
        #pragma clang diagnostic push
        #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [self performSelector:sel withObject:nil];
        #pragma clang diagnostic pop
    }

但这种情况下,变量 c 不会被自动俘获。如果你在 jsq_setToolbarBottomLayoutGuideConstant: 方法的第一行代码加上断点运行程序,当程序运行到断点处时,打印参数的值,你会发现其值为 NaN ,如果继续执行代码,App 会崩溃。

这种情况下,我们无法使用自动俘获,因此只能使用 withObject 来传递参数了。但由于 CGFloat 不是 NSObject,无法用 [performSelector: withObjectd:] 来传参,因此要使用 NSInvocation 来调用:

-(void)setToolbarSpaceToBottom:(CGFloat)constant{

    SEL sel=NSSelectorFromString(@"jsq_setToolbarBottomLayoutGuideConstant:");

    NSInvocation *invoc = [NSInvocation invocationWithMethodSignature:[[self class] instanceMethodSignatureForSelector:sel]];

    [invoc setSelector:sel];
    [invoc setTarget:self];

    [invoc setArgument:&constant atIndex:2];//"Indices 0 and 1 indicate the hidden arguments self and _cmd"

    [invoc performSelector:@selector(invoke) withObject:nil];
}

你可能感兴趣的:(performSelector 方法的自动俘获特性)