[爆棧熱門 iOS 問題] performSelector may cause a leak because its selector is unknown

系列文集:爆棧熱門 iOS 問題目錄在此。倉薯翻譯,歡迎指正:)

問題

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

"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:

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks" [self.ticketTarget performSelector: self.ticketAction withObject: self];
#pragma clang diagnostic pop

如果在多個地方都要用,可以定義一個宏:

#define SuppressPerformSelectorLeakWarning(Stuff) \
    do { \
        _Pragma("clang diagnostic push") \
        _Pragma("clang diagnostic ignored \"-Warc-performSelector-leaks\"") \
        Stuff; \
        _Pragma("clang diagnostic pop") \
    } while (0)

用的時候:

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 的全部信息,不需要默認任何事情。

倉薯注:后面作者還寫了點歷史,我就沒有翻譯了,感興趣請前往原文閱讀。


原文地址:performSelector may cause a leak because its selector is unknown

本文地址:http://www.lxweimin.com/p/6517ab655be7

系列文集:爆棧熱門 iOS 問題

譯者:@戴倉薯


  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 返回值?

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 轉至元數據結尾創建: 董瀟偉,最新修改于: 十二月 23, 2016 轉至元數據起始第一章:isa和Class一....
    40c0490e5268閱讀 1,757評論 0 9
  • 問題描述 項目中使用到了從字符串創建選擇器,編譯時發現警告:"performSelector may cause ...
    ttdiOS閱讀 564評論 0 1
  • __block和__weak修飾符的區別其實是挺明顯的:1.__block不管是ARC還是MRC模式下都可以使用,...
    LZM輪回閱讀 3,354評論 0 6
  • *面試心聲:其實這些題本人都沒怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個offer,總結起來就是把...
    Dove_iOS閱讀 27,199評論 30 471
  • 火車站,多的是一些離別的人和故事。記得大學畢業那年,我的大學閨蜜kk到火車站送我。本以為會上演一幕生離死別、淚水漣...
    七蕭蕭閱讀 477評論 0 1