偶然在调试中发现,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];
}