我在 ARC 模式下編譯出了這個 warning:html
"performSelector may cause a leak because its selector is unknown".
個人代碼是這麼寫的:安全
[_controller performSelector:NSSelectorFromString(@"someMethod")];
爲何會有這個 warning 呢?我知道編譯器沒法檢查實際上有沒有這個 selector,不過這爲何會形成內存泄漏呢?代碼應該怎麼改才能消除這個 warning?函數
Scott Thompson,1100 票指針
LLVM 3.0 編譯器能夠用如下代碼消除 warning:code
#pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" [self.ticketTarget performSelector: self.ticketAction withObject: self]; #pragma clang diagnostic pop
若是在多個地方都要用,能夠定義一個宏:orm
#define SuppressPerformSelectorLeakWarning(Stuff) \ do { \ _Pragma("clang diagnostic push") \ _Pragma("clang diagnostic ignored \"-Warc-performSelector-leaks\"") \ Stuff; \ _Pragma("clang diagnostic pop") \ } while (0)
用的時候:htm
SuppressPerformSelectorLeakWarning( [_target performSelector:_action withObject:self] );
若是須要返回值:對象
id result; SuppressPerformSelectorLeakWarning( result = [_target performSelector:_action withObject:self] );
wbyoung,768 贊內存
編譯器報這個 warning 是有緣由的,通常不該該直接忽略,並且消除這個 warning 並不難。以下便可:文檔
if (!_controller) { return; } SEL selector = NSSelectorFromString(@"someMethod"); IMP imp = [_controller methodForSelector:selector]; void (*func)(id, SEL) = (void *)imp; func(_controller, selector);
或者寫得緊密一些(不過可讀性差一些,也少了類型檢查):
SEL selector = NSSelectorFromString(@"someMethod"); ((void (*)(id, SEL))[_controller methodForSelector:selector])(_controller, selector);
這一堆代碼在作的事情實際上是,向 controller 請求那個方法對應的 C 函數指針。全部的NSObject
都能響應methodForSelector:
這個方法,不過也能夠用 Objective-C runtime 裏的class_getMethodImplementation
(只在 protocol 的狀況下有用,id<SomeProto>
這樣的)。這種函數指針叫作IMP
,就是typedef
過的函數指針(id (*IMP)(id, SEL, ...)
[1])。它跟方法簽名(signature)比較像,雖然可能不是徹底同樣。
獲得IMP
以後,還須要進行轉換,轉換後的函數指針包含 ARC 所需的那些細節(好比每一個 OC 方法調用都有的兩個隱藏參數self
和_cmd
)。這就是代碼第 4 行乾的事(右邊的那個(void *)
只是告訴編譯器,不用報類型強轉的 warning)。
最後一步,調用函數指針[2]。
若是 selector 接收參數,或者有返回值,代碼就須要改改:
SEL selector = NSSelectorFromString(@"processRegion:ofView:"); IMP imp = [_controller methodForSelector:selector]; CGRect (*func)(id, SEL, CGRect, UIView *) = (void *)imp; CGRect result = _controller ? func(_controller, selector, someRect, someView) : CGRectZero;
緣由是這樣的:咱們在 ARC 下調一個方法,runtime 須要知道對於返回值該怎麼辦。返回值可能有各類類型:void
,int
,char
,NSString *
,id
等等。ARC 通常是根據返回值的頭文件來決定該怎麼辦的[3],一共有如下 4 種狀況[4]:
void
,int
這樣的)。init
、copy
這一類的方法,或者標註ns_returns_retained
的方法)。ns_returns_autoreleased
的方法)。而調performSelector:
的時候,系統會默認返回值並非基本類型,但也不會 retain、release,也就是默認採起第 4 種作法。因此若是那個方法原本應該屬於前 3 種狀況,都有可能會形成內存泄漏。
對於返回void
或者基本類型的方法,就目前而言你能夠忽略這個 warning,但這樣作不必定安全。我看過 Clang 在處理返回值這塊兒的幾回迭代演進。一旦開着 ARC,編譯器會以爲從performSelector:
返回的對象沒理由不能 retain,不能 release。在編譯器眼裏,它就是個對象。因此,若是返回值是基本類型或者void
,編譯器仍是存在會 retain、release 它的可能,而後直接致使 crash。
相似地,performSelector:withObject:
也會報同一個 warning,由於不指明怎麼處理參數也會有一樣的問題。ARC 容許爲方法參數標註consumed
,若是你調的方法有這種標註,最終可能致使把消息發給殭屍對象而後 crash。要解決這個問題能夠用橋接(bridged casting),可是最好最簡單的方法仍是我上面寫的用IMP
和函數指針的方法。不過給參數標 consumed 是比較少見的,因此這個問題也不容易發生。
有趣的是,下面這種靜態聲明的 selector 就不會出 warning:
[_controller performSelector:@selector(someMethod)];
緣由是,這種狀況下編譯器就能在編譯階段獲得關於這個 selector 的所有信息,不須要默認任何事情。
[1]: 全部的 Objective-C 方法都有兩個隱藏的參數,self
和_cmd
,調用時自動加的。 ↩
[2]: 在 C 裏調用NULL
方法是不安全的。而if (!_controller) { return; }
這一句保證controller
不爲空,因此咱們必定能從methodForSelector:
獲得一個IMP
(雖然可能只是_objc_msgForward
,進入消息轉發系統)。基本上,有了這行檢查,就能保證咱們有方法可調。 ↩
[3]: 實際上,若是返回值的類型是id
,而你又沒 import 對應的頭文件,它是有可能作出錯誤處理的。有可能會 crash 在一塊編譯器覺得安全的代碼裏。這種狀況很罕見,但仍是有發生的可能。通常來講,若是編譯器不知道該選哪一個方法簽名,它會報一個 warning 的。 ↩
[4]: 更多細節請參考 ARC 的文檔 retain 返回值 和 不 retain 返回值。 ↩