performSelector may cause a leak because its selector is unknown

轉自:http://www.jianshu.com/p/6517ab655be7

問題

我在 ARC 模式下編譯出了這個 warning:html

"performSelector may cause a leak because its selector is unknown".

個人代碼是這麼寫的:安全

[_controller performSelector:NSSelectorFromString(@"someMethod")];

爲何會有這個 warning 呢?我知道編譯器沒法檢查實際上有沒有這個 selector,不過這爲何會形成內存泄漏呢?代碼應該怎麼改才能消除這個 warning?函數


答案

答案1:單純消除 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]
);

答案2:詳細解釋和正統解決

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;

爲何會有這個 warning

緣由是這樣的:咱們在 ARC 下調一個方法,runtime 須要知道對於返回值該怎麼辦。返回值可能有各類類型:voidintcharNSString *id等等。ARC 通常是根據返回值的頭文件來決定該怎麼辦的[3],一共有如下 4 種狀況[4]

  1. 直接忽略(若是是基本類型好比 voidint這樣的)。
  2. 把返回值先 retain,等到用不到的時候再 release(最多見的狀況)。
  3. 不 retain,等到用不到的時候直接 release(用於 initcopy 這一類的方法,或者標註ns_returns_retained的方法)。
  4. 什麼也不作,默認返回值在返回先後是始終有效的(一直到最近的 release pool 結束爲止,用於標註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

有趣的是,下面這種靜態聲明的 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 返回值

相關文章
相關標籤/搜索