ios - 由於其選擇器未知,performSelector可能會導致洩漏




objective-c memory-leaks (13)

不要壓制警告!

有不少於12種替代解決方案來修補編譯器。
儘管在第一次實施的時候你很聰明,但地球上很少有工程師可以追隨你的腳步,而這些代碼最終會破滅。

安全路線:

所有這些解決方案都可以工作,與您原來的意圖有一定程度的差異。 假設如果你願意, param可以是nil

安全路線,相同的概念行為:

// GREAT
[_controller performSelectorOnMainThread:selector withObject:anArgument waitUntilDone:YES];
[_controller performSelectorOnMainThread:selector withObject:anArgument waitUntilDone:YES modes:@[(__bridge NSString *)kCFRunLoopDefaultMode]];

[_controller performSelector:selector onThread:[NSThread mainThread] withObject:anArgument waitUntilDone:YES];
[_controller performSelector:selector onThread:[NSThread mainThread] withObject:anArgument waitUntilDone:YES modes:@[(__bridge NSString *)kCFRunLoopDefaultMode]];

安全路線,行為略有不同:

(請參閱this答复)
使用任何線程來代替[NSThread mainThread]

// GOOD
[_controller performSelector:selector withObject:anArgument afterDelay:0];
[_controller performSelector:selector withObject:anArgument afterDelay:0 inModes:@[(__bridge NSString *)kCFRunLoopDefaultMode]];

[_controller performSelectorOnMainThread:selector withObject:anArgument waitUntilDone:NO];
[_controller performSelectorOnMainThread:selector withObject:anArgument waitUntilDone:NO];
[_controller performSelectorOnMainThread:selector withObject:anArgument waitUntilDone:NO modes:@[(__bridge NSString *)kCFRunLoopDefaultMode]];

[_controller performSelectorInBackground:selector withObject:anArgument];

[_controller performSelector:selector onThread:[NSThread mainThread] withObject:anArgument waitUntilDone:NO];
[_controller performSelector:selector onThread:[NSThread mainThread] withObject:anArgument waitUntilDone:NO modes:@[(__bridge NSString *)kCFRunLoopDefaultMode]];

危險路線

需要某種編譯器的沉默,這一定會中斷。 請注意,目前,它確實Swift中斷了。

// AT YOUR OWN RISK
[_controller performSelector:selector];
[_controller performSelector:selector withObject:anArgument];
[_controller performSelector:selector withObject:anArgument withObject:nil];

我收到ARC編譯器的以下警告:

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

以下是我正在做的事情:

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

為什麼我會收到此警告? 我知道編譯器不能檢查選擇器是否存在,但為什麼會導致洩漏? 我怎樣才能改變我的代碼,使我不再接受這個警告?


編譯器出於某種原因警告這個問題。 這種警告很容易被忽略,這很容易解決。 就是這樣:

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);

說明

這裡發生的是你要求控制器為控制器對應的方法指定C函數指針。 所有的NSObjectmethodForSelector:響應,但是你也可以在Objective-C運行庫中使用class_getMethodImplementation (如果你只有一個協議引用,比如id<SomeProto> ),那麼它很有用。 這些函數指針被稱為IMP ,並且是簡單的typedef函數指針( id (*IMP)(id, SEL, ...)1 。 這可能接近方法的實際方法簽名,但並不總是完全匹配。

一旦你擁有了IMP ,你需要將它轉換成一個函數指針,該指針包含ARC需要的所有細節(包括每個Objective-C方法調用的隱含的隱含參數self_cmd )。 這是在第三行中處理的(void *)右邊的(void *)只是告訴編譯器,你知道你在做什麼,並且不會因為指針類型不匹配而產生警告)。

最後,你調用函數指針2

複雜的例子

當選擇器接受參數或返回一個值時,你必須改變一點:

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時,運行時需要知道如何處理您調用的方法的結果。 結果可能是任何東西: voidintcharNSString *id等。ARC通常從您正在處理的對像類型的標題中獲取此信息。 3

實際上ARC只會考慮4個返回值: 4

  1. 忽略非對像類型( voidint等)
  2. 保留對象值,然後在不再使用時釋放(標准假設)
  3. 不再使用時釋放新的對象值( init / copy系列中的方法或由ns_returns_retained
  4. 不要做任何事情,並假設返回的對象值在本地範圍內有效(直到內部最多的發布池被耗盡,歸因於ns_returns_autoreleased

methodForSelector:的調用methodForSelector:假定它所調用的方法的返回值是一個對象,但不保留/釋放它。 所以,如果你的對象應該像上面#3那樣被釋放(也就是說,你調用的方法返回一個新對象),那麼你最終可能會創建一個洩漏。

對於試圖調用返回void或其他非對象的選擇器,可以啟用編譯器功能來忽略該警告,但這可能很危險。 我已經看到Clang經歷了幾次迭代,它處理了未分配給局部變量的返回值。 沒有理由在啟用ARC的情況下無法保留並釋放methodForSelector:返回的對象值methodForSelector:即使您不想使用它。 從編譯器的角度來看,它畢竟是一個對象。 這意味著如果您調用的方法someMethod正在返回一個非對象(包括void ),則最終可能會保留/釋放垃圾指針值並導致崩潰。

其他參數

一個需要注意的是,這與performSelector:withObject:會發生相同的警告performSelector:withObject:並且您可能會遇到類似的問題,而不會聲明該方法如何消耗參數。 ARC允許聲明消耗的參數 ,並且如果該方法使用該參數,則最終可能會向殭屍發送消息並崩潰。 有很多方法可以通過橋接模式來解決這個問題,但是真的使用上面的IMP和函數指針方法會更好。 由於消耗的參數很少成為問題,所以不太可能出現。

靜態選擇器

有趣的是,編譯器不會抱怨靜態聲明的選擇器:

[_controller performSelector:@selector(someMethod)];

原因是編譯器實際上能夠在編譯期間記錄有關選擇器和對象的所有信息。 它不需要對任何事情做任何假設。 (我通過查看源代碼在一年前檢查過這一年,但現在沒有參考。)

抑制

在試圖想要抑制這種警告是必要的並且良好的代碼設計的時候,我會空白。 有人請分享,如果他們有經驗沉默這個警告是必要的(上面沒有正確處理事情)。

更多

也可以建立一個NSMethodInvocation來處理這個問題,但這樣做需要更多的輸入,而且速度也更慢,所以沒有什麼理由要這樣做。

歷史

performSelector:方法系列首次添加到Objective-C時,ARC不存在。 在創建ARC時,Apple決定應該為這些方法生成警告,作為指導開發人員使用其他方法明確定義在通過命名選擇器發送任意消息時應如何處理內存的方式。 在Objective-C中,開發人員可以通過在原始函數指針上使用C風格轉換來實現此目的。

隨著Swift的推出,Apple將performSelector:方法族記錄為“固有不安全”,並且它們不適用於Swift。

隨著時間的推移,我們看到了這種進展:

  1. 早期版本的Objective-C允許performSelector:手動內存管理)
  2. Objective-C與ARC警告使用performSelector:
  3. Swift無法訪問performSelector:並將這些方法記錄為“固有不安全”

然而,基於命名選擇器發送消息的想法並不是“固有的不安全”功能。 這個想法在Objective-C以及許多其他編程語言中已經成功使用了很長時間。

1所有Objective-C方法都有兩個隱藏參數, self_cmd ,當您調用方法時隱式添加它們。

2在C中調用一個NULL函數是不安全的。用於檢查控制器是否存在的警衛確保我們有一個對象。 因此,我們知道我們將從methodForSelector:獲得一個IMP methodForSelector:儘管它可能是_objc_msgForward ,進入消息轉發系統)。 基本上,有了守衛,我們知道我們有一個功能可以打電話。

3實際上,如果將對象聲明為id並且您沒有導入所有標頭,則可能會得到錯誤的信息。 編譯器認為是好的,你可能會在代碼中崩潰。 這是非常罕見的,但可能發生。 通常你會得到一個警告,它不知道兩個方法簽名中哪一個可以選擇。

4有關更多詳細信息,請參閱保留返回值和未保留返回值的ARC參考。


Matt Galloway在這個主題上的回答解釋了為什麼:

考慮以下:

id anotherObject1 = [someObject performSelector:@selector(copy)];
id anotherObject2 = [someObject performSelector:@selector(giveMeAnotherNonRetainedObject)];

現在,ARC如何知道第一個返回保留計數為1的對象,但第二個返回的是一個自動釋放的對象?

如果您忽略了返回值,似乎通常會安全地取消警告。 我不確定最好的做法是什麼,如果你真的需要從performSelector獲取保留的對象 - 除了“不這樣做”。


If you don't need to pass any arguments an easy workaround is to use valueForKeyPath . This is even possible on a Class object.

NSString *colorName = @"brightPinkColor";
id uicolor = [UIColor class];
if ([uicolor respondsToSelector:NSSelectorFromString(colorName)]){
    UIColor *brightPink = [uicolor valueForKeyPath:colorName];
    ...
}

You could also use a protocol here. So, create a protocol like so:

@protocol MyProtocol
-(void)doSomethingWithObject:(id)object;
@end

In your class that needs to call your selector, you then have a @property.

@interface MyObject
    @property (strong) id<MyProtocol> source;
@end

When you need to call @selector(doSomethingWithObject:) in an instance of MyObject, do this:

[self.source doSomethingWithObject:object];

作為解決方法,直到編譯器允許覆蓋警告為止,您可以使用運行時

objc_msgSend(_controller, NSSelectorFromString(@"someMethod"));

代替

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

你必須

#import <objc/message.h>


在您的項目構建設置中 ,在其他警告標誌WARNING_CFLAGS )下,添加
-Wno-arc-performSelector-leaks

現在只要確保您所調用的選擇器不會導致您的對像被保留或複制。


奇怪但真實的:如果可以接受的話(即結果是無效的,你不介意讓runloop循環一次),增加一個延遲,即使它是零:

[_controller performSelector:NSSelectorFromString(@"someMethod")
    withObject:nil
    afterDelay:0];

這可以消除警告,大概是因為它可以讓編譯器確信沒有任何對象可以被返回並以某種方式被錯誤地管理。


我對此的猜測是:由於編譯器不知道選擇器,因此ARC無法執行正確的內存管理。

事實上,有些時候內存管理是通過特定的約定與方法的名稱相關聯的。 具體來說,我正在考慮便捷的構造函數make方法; 前者按慣例返回自動放棄的對象; 後者是保留的對象。 約定基於選擇器的名稱,所以如果編譯器不知道選擇器,那麼它不能執行正確的內存管理規則。

如果這是正確的,我認為你可以安全地使用你的代碼,只要你確保在內存管理方面一切正常(例如,你的方法不返回它們分配的對象)。


此代碼不涉及編譯器標誌或直接運行時調用:

SEL selector = @selector(zeroArgumentMethod);
NSMethodSignature *methodSig = [[self class] instanceMethodSignatureForSelector:selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setSelector:selector];
[invocation setTarget:self];
[invocation invoke];

NSInvocation允許設置多個參數,所以與performSelector不同,這將適用於任何方法。


為了讓Scott Thompson的宏觀更加通用:

// String expander
#define MY_STRX(X) #X
#define MY_STR(X) MY_STRX(X)

#define MYSilenceWarning(FLAG, MACRO) \
_Pragma("clang diagnostic push") \
_Pragma(MY_STR(clang diagnostic ignored MY_STR(FLAG))) \
MACRO \
_Pragma("clang diagnostic pop")

然後像這樣使用它:

MYSilenceWarning(-Warc-performSelector-leaks,
[_target performSelector:_action withObject:self];
                )

由於您使用ARC,您必須使用iOS 4.0或更高版本。 這意味著你可以使用塊。 如果不是記住選擇器來執行,而是採取了一個塊,ARC將能夠更好地跟踪實際正在發生的事情,並且不必冒意外引入內存洩漏的風險。


這是根據上面給出的答案更新的宏。 這個應該允許你用return語句包裝你的代碼。

#define SUPPRESS_PERFORM_SELECTOR_LEAK_WARNING(code)                        \
    _Pragma("clang diagnostic push")                                        \
    _Pragma("clang diagnostic ignored \"-Warc-performSelector-leaks\"")     \
    code;                                                                   \
    _Pragma("clang diagnostic pop")                                         \


SUPPRESS_PERFORM_SELECTOR_LEAK_WARNING(
    return [_target performSelector:_action withObject:self]
);






automatic-ref-counting