系列文集:爆棧熱門 iOS 問題渐扮。目錄在此饥悴。倉薯翻譯送漠,歡迎指正:)
問題
我在 ARC 模式下編譯出了這個 warning:
"performSelector may cause a leak because its selector is unknown".
我的代碼是這么寫的:
[_controller performSelector:NSSelectorFromString(@"someMethod")];
為什么會有這個 warning 呢粉渠?我知道編譯器無法檢查實際上有沒有這個 selector,不過這為什么會造成內(nèi)存泄漏呢派哲?代碼應(yīng)該怎么改才能消除這個 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:詳細(xì)解釋和正統(tǒng)解決
wbyoung,768 贊
解決方案
編譯器報這個 warning 是有原因的巢寡,一般不應(yīng)該直接忽略喉脖,而且消除這個 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 請求那個方法對應(yīng)的 C 函數(shù)指針。所有的NSObject
都能響應(yīng)methodForSelector:
這個方法谦絮,不過也可以用 Objective-C runtime 里的class_getMethodImplementation
(只在 protocol 的情況下有用题诵,id<SomeProto>
這樣的)。這種函數(shù)指針叫做IMP
层皱,就是typedef
過的函數(shù)指針(id (*IMP)(id, SEL, ...)
[1])性锭。它跟方法簽名(signature)比較像,雖然可能不是完全一樣叫胖。
得到IMP
之后草冈,還需要進(jìn)行轉(zhuǎn)換,轉(zhuǎn)換后的函數(shù)指針包含 ARC 所需的那些細(xì)節(jié)(比如每個 OC 方法調(diào)用都有的兩個隱藏參數(shù)self
和_cmd
)。這就是代碼第 4 行干的事(右邊的那個(void *)
只是告訴編譯器怎棱,不用報類型強(qiáng)轉(zhuǎn)的 warning)哩俭。
最后一步,調(diào)用函數(shù)指針[2]拳恋。
更復(fù)雜的例子
如果 selector 接收參數(shù)凡资,或者有返回值,代碼就需要改改:
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 下調(diào)一個方法谬运,runtime 需要知道對于返回值該怎么辦隙赁。返回值可能有各種類型:void
,int
梆暖,char
伞访,NSString *
,id
等等式廷。ARC 一般是根據(jù)返回值的頭文件來決定該怎么辦的[3]咐扭,一共有以下 4 種情況[4]:
- 直接忽略(如果是基本類型比如
void
,int
這樣的)滑废。 - 把返回值先 retain蝗肪,等到用不到的時候再 release(最常見的情況)。
- 不 retain蠕趁,等到用不到的時候直接 release(用于
init
薛闪、copy
這一類的方法,或者標(biāo)注ns_returns_retained
的方法)俺陋。 - 什么也不做豁延,默認(rèn)返回值在返回前后是始終有效的(一直到最近的 release pool 結(jié)束為止,用于標(biāo)注
ns_returns_autoreleased
的方法)腊状。
而調(diào)performSelector:
的時候诱咏,系統(tǒng)會默認(rèn)返回值并不是基本類型,但也不會 retain缴挖、release袋狞,也就是默認(rèn)采取第 4 種做法。所以如果那個方法本來應(yīng)該屬于前 3 種情況映屋,都有可能會造成內(nèi)存泄漏苟鸯。
對于返回void
或者基本類型的方法,就目前而言你可以忽略這個 warning棚点,但這樣做不一定安全早处。我看過 Clang 在處理返回值這塊兒的幾次迭代演進(jìn)。一旦開著 ARC瘫析,編譯器會覺得從performSelector:
返回的對象沒理由不能 retain砌梆,不能 release默责。在編譯器眼里,它就是個對象么库。所以傻丝,如果返回值是基本類型或者void
甘有,編譯器還是存在會 retain诉儒、release 它的可能,然后直接導(dǎo)致 crash亏掀。
帶參數(shù)調(diào)用
類似地忱反,performSelector:withObject:
也會報同一個 warning,因為不指明怎么處理參數(shù)也會有同樣的問題滤愕。ARC 允許為方法參數(shù)標(biāo)注consumed
温算,如果你調(diào)的方法有這種標(biāo)注,最終可能導(dǎo)致把消息發(fā)給僵尸對象然后 crash间影。要解決這個問題可以用橋接(bridged casting)注竿,但是最好最簡單的方法還是我上面寫的用IMP
和函數(shù)指針的方法。不過給參數(shù)標(biāo) consumed 是比較少見的魂贬,所以這個問題也不容易發(fā)生巩割。
靜態(tài) selector
有趣的是,下面這種靜態(tài)聲明的 selector 就不會出 warning:
[_controller performSelector:@selector(someMethod)];
原因是付燥,這種情況下編譯器就能在編譯階段得到關(guān)于這個 selector 的全部信息宣谈,不需要默認(rèn)任何事情。
倉薯注:后面作者還寫了點歷史键科,我就沒有翻譯了闻丑,感興趣請前往原文閱讀。
原文地址:performSelector may cause a leak because its selector is unknown
系列文集:爆棧熱門 iOS 問題
譯者:@戴倉薯
-
所有的 Objective-C 方法都有兩個隱藏的參數(shù)勋颖,
self
和_cmd
嗦嗡,調(diào)用時自動加的。 ? -
在 C 里調(diào)用
NULL
方法是不安全的饭玲。而if (!_controller) { return; }
這一句保證controller
不為空侥祭,所以我們一定能從methodForSelector:
得到一個IMP
(雖然可能只是_objc_msgForward
,進(jìn)入消息轉(zhuǎn)發(fā)系統(tǒng))咱枉”傲颍基本上,有了這行檢查蚕断,就能保證我們有方法可調(diào)欢伏。 ? -
實際上,如果返回值的類型是
id
亿乳,而你又沒 import 對應(yīng)的頭文件硝拧,它是有可能做出錯誤處理的径筏。有可能會 crash 在一塊編譯器以為安全的代碼里。這種情況很罕見障陶,但還是有發(fā)生的可能滋恬。一般來說,如果編譯器不知道該選哪個方法簽名抱究,它會報一個 warning 的恢氯。 ? -
更多細(xì)節(jié)請參考 ARC 的文檔 retain 返回值 和 不 retain 返回值。 ?