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



objective-c memory-leaks automatic-ref-counting (17)

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

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

以下是我正在做的事情:

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

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


Answers

為了讓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];
                )

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

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


這是根據上面給出的答案更新的宏。 這個應該允許你用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]
);

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

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];
    ...
}

Instead of using the block approach, which gave me some problems:

    IMP imp = [_controller methodForSelector:selector];
    void (*func)(id, SEL) = (void *)imp;

I will use NSInvocation, like this:

    -(void) sendSelectorToDelegate:(SEL) selector withSender:(UIButton *)button 

    if ([delegate respondsToSelector:selector])
    {
    NSMethodSignature * methodSignature = [[delegate class]
                                    instanceMethodSignatureForSelector:selector];
    NSInvocation * delegateInvocation = [NSInvocation
                                   invocationWithMethodSignature:methodSignature];


    [delegateInvocation setSelector:selector];
    [delegateInvocation setTarget:delegate];

    // remember the first two parameter are cmd and self
    [delegateInvocation setArgument:&button atIndex:2];
    [delegateInvocation invoke];
    }

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

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參考。


要僅使用執行選擇器忽略文件中的錯誤,請按如下方式添加#pragma:

#pragma clang diagnostic ignored "-Warc-performSelector-leaks"

這會忽略這條線上的警告,但仍然允許在整個項目的其餘部分。


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

考慮以下:

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

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

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


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

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

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


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


不要壓制警告!

有不少於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];

為了後代的緣故,我決定把我的帽子扔進戒指:)

最近我已經看到越來越多的重構遠離target / selector範例,支持諸如協議,塊等等。然而,現在我已經使用了幾次的performSelector替代方案:

[NSApp sendAction: NSSelectorFromString(@"someMethod") to: _controller from: nil];

這些似乎是一個乾淨的,ARC安全的,幾乎相同的替代performSelector而不必與objc_msgSend()多少有關。

雖然,我不知道iOS上是否有可用的模擬器。


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

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

代替

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

你必須

#import <objc/message.h>


@ c-road在此提供與問題描述的正確鏈接。 下面你可以看到我的例子,當performSelector導致內存洩漏。

@interface Dummy : NSObject <NSCopying>
@end

@implementation Dummy

- (id)copyWithZone:(NSZone *)zone {
  return [[Dummy alloc] init];
}

- (id)clone {
  return [[Dummy alloc] init];
}

@end

void CopyDummy(Dummy *dummy) {
  __unused Dummy *dummyClone = [dummy copy];
}

void CloneDummy(Dummy *dummy) {
  __unused Dummy *dummyClone = [dummy clone];
}

void CopyDummyWithLeak(Dummy *dummy, SEL copySelector) {
  __unused Dummy *dummyClone = [dummy performSelector:copySelector];
}

void CloneDummyWithoutLeak(Dummy *dummy, SEL cloneSelector) {
  __unused Dummy *dummyClone = [dummy performSelector:cloneSelector];
}

int main(int argc, const char * argv[]) {
  @autoreleasepool {
    Dummy *dummy = [[Dummy alloc] init];
    for (;;) { @autoreleasepool {
      //CopyDummy(dummy);
      //CloneDummy(dummy);
      //CloneDummyWithoutLeak(dummy, @selector(clone));
      CopyDummyWithLeak(dummy, @selector(copy));
      [NSThread sleepForTimeInterval:1];
    }} 
  }
  return 0;
}

在我的例子中導致內存洩漏的唯一方法是CopyDummyWithLeak。 原因是ARC不知道,copySelector返回保留對象。

如果你運行內存洩漏工具,你可以看到下面的圖片: ...在任何其他情況下都沒有內存洩漏:


好吧,這裡有很多答案,但是因為這有點不同,結合了一些我認為我會介紹的答案。我使用了一個NSObject類來檢查選擇器是否返回void,並且還抑制了編譯器警告。

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import "Debug.h" // not given; just an assert

@interface NSObject (Extras)

// Enforce the rule that the selector used must return void.
- (void) performVoidReturnSelector:(SEL)aSelector withObject:(id)object;
- (void) performVoidReturnSelector:(SEL)aSelector;

@end

@implementation NSObject (Extras)

// Apparently the reason the regular performSelect gives a compile time warning is that the system doesn't know the return type. I'm going to (a) make sure that the return type is void, and (b) disable this warning
// See http://.com/questions/7017281/performselector-may-cause-a-leak-because-its-selector-is-unknown

- (void) checkSelector:(SEL)aSelector {
    // See http://.com/questions/14602854/objective-c-is-there-a-way-to-check-a-selector-return-value
    Method m = class_getInstanceMethod([self class], aSelector);
    char type[128];
    method_getReturnType(m, type, sizeof(type));

    NSString *message = [[NSString alloc] initWithFormat:@"NSObject+Extras.performVoidReturnSelector: %@.%@ selector (type: %s)", [self class], NSStringFromSelector(aSelector), type];
    NSLog(@"%@", message);

    if (type[0] != 'v') {
        message = [[NSString alloc] initWithFormat:@"%@ was not void", message];
        [Debug assertTrue:FALSE withMessage:message];
    }
}

- (void) performVoidReturnSelector:(SEL)aSelector withObject:(id)object {
    [self checkSelector:aSelector];

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    // Since the selector (aSelector) is returning void, it doesn't make sense to try to obtain the return result of performSelector. In fact, if we do, it crashes the app.
    [self performSelector: aSelector withObject: object];
#pragma clang diagnostic pop    
}

- (void) performVoidReturnSelector:(SEL)aSelector {
    [self checkSelector:aSelector];

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    [self performSelector: aSelector];
#pragma clang diagnostic pop
}

@end

ARC不會幫助你處理非ObjC內存,例如,如果你使用malloc() ,你仍然需要free()它。

ARC可以被performSelector:愚弄performSelector:如果編譯器無法弄清楚選擇器是什麼(編譯器會在那裡產生一個警告)。

ARC還會根據ObjC命名約定生成代碼,所以如果你混合使用ARC和MRC代碼,如果MRC代碼沒有執行編譯器認為的命名所允許的結果,你會得到令人驚訝的結果。





ios objective-c memory-leaks automatic-ref-counting