1. 程式人生 > >performSelector 方法的自動俘獲特性

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