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];
}