performSelector可能导致泄漏,因为其选择器未知

时间:2011-08-10 20:23:24

标签: ios objective-c memory-leaks automatic-ref-counting

ARC编译器收到以下警告:

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

这就是我正在做的事情:

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

为什么我会收到此警告?我理解编译器无法检查选择器是否存在,但为什么会导致泄漏?如何更改我的代码,以便我不再收到此警告?

19 个答案:

答案 0 :(得分:1186)

解决方案

由于某种原因,编译器会对此发出警告。这个警告应该被忽略,这很容易解决,这是非常罕见的。以下是:

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函数指针。所有NSObject都响应methodForSelector:,但您也可以在Objective-C运行时使用class_getMethodImplementation(如果您只有协议引用,则非常有用,如id<SomeProto>)。这些函数指针称为IMP s,是简单的typedef ed函数指针(id (*IMP)(id, SEL, ...) 1 。这可能接近方法的实际方法签名,但并不总是完全匹配。

获得IMP之后,需要将其强制转换为包含ARC所需的所有详细信息的函数指针(包括隐含的两个隐含参数self_cmd每个Objective-C方法调用)。这是在第三行处理的(右侧的(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
  5. methodForSelector:的调用假定它调用的方法的返回值是一个对象,但不保留/释放它。因此,如果您的对象应该像上面的#3那样被释放(也就是说,您正在调用的方法返回一个新对象),那么最终可能会创建泄漏。

    对于您尝试调用返回void或其他非对象的选择器,您可以启用编译器功能来忽略警告,但这可能很危险。我已经看到Clang经历了一些迭代,它处理了如何处理没有分配给局部变量的返回值。如果启用ARC,则无法保留和释放methodForSelector:返回的对象值,即使您不想使用它也是如此。从编译器的角度来看,它毕竟是一个对象。这意味着如果您调用的方法someMethod正在返回非对象(包括void),则最终可能会保留/释放垃圾指针值并崩溃。 / p>

    附加参数

    一个考虑因素是performSelector:withObject:会发生同样的警告,你可能会遇到类似的问题而没有声明该方法如何使用参数。 ARC允许声明consumed parameters,如果方法使用参数,您最终可能会向僵尸发送消息并崩溃。有一些方法可以通过桥接转换来解决这个问题,但实际上只需使用上面的IMP和函数指针方法就更好了。由于消耗的参数很少成为问题,因此不太可能出现。

    静态选择器

    有趣的是,编译器不会抱怨静态声明的选择器:

    [_controller performSelector:@selector(someMethod)];
    

    原因是编译器实际上能够在编译期间记录有关选择器和对象的所有信息。它不需要对任何事情作出任何假设。 (我之前通过查看来源检查了一年,但现在没有参考。)

    抑制

    在试图考虑抑制此警告是必要的以及良好的代码设计的情况时,我发现空白。有人请分享,如果他们有经验需要消除此警告(以上情况并未妥善处理)。

    更多

    可以建立一个NSMethodInvocation来处理这个问题,但这样做需要更多的打字并且速度也慢一些,所以没有什么理由去做。< / p>

    历史

    当首次将performSelector:系列方法添加到Objective-C时,ARC不存在。在创建ARC时,Apple决定应为这些方法生成警告,以指导开发人员使用其他方法明确定义在通过命名选择器发送任意消息时应如何处理内存。在Objective-C中,开发人员可以通过在原始函数指针上使用C样式转换来实现此目的。

    随着Swift的介绍,Apple has documented performSelector:系列方法为&#34;固有的不安全&#34;并且它们不适用于Swift。

    随着时间的推移,我们已经看到了这种进展:

    1. 早期版本的Objective-C允许performSelector:(手动内存管理)
    2. 使用ARC的Objective-C警告使用performSelector:
    3. Swift无权访问performSelector:并将这些方法记录为&#34;本质上不安全&#34;
    4. 然而,基于命名选择器发送消息的想法并不是“本质上不安全的”#34;特征。这个想法已经在Objective-C以及许多其他编程语言中成功使用了很长时间。


      1 所有Objective-C方法都有两个隐藏的参数,self_cmd,在调用方法时会隐式添加。

      2 在C中调用NULL函数是不安全的。用于检查控制器是否存在的防护确保我们有一个对象。因此,我们知道我们会从IMP获得methodForSelector:(尽管它可能是_objc_msgForward,但要进入邮件转发系统)。基本上,在守卫到位的情况下,我们知道我们有一个可以打电话的功能。

      3 实际上,如果您将对象声明为id并且您没有导入所有标头,则可能会收到错误的信息。您最终可能会遇到编译器认为正常的代码崩溃。这种情况非常罕见,但可能会发生。通常,您只会收到一条警告,表示它不知道可以选择哪两种方法签名。

      4 有关详细信息,请参阅retained return valuesunretained return values上的ARC参考。

答案 1 :(得分:1178)

在Xcode 4.2中的LLVM 3.0编译器中,您可以按如下方式禁止警告:

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

如果您在多个地方收到错误,并希望使用C宏系统隐藏编译指示,则可以定义一个宏以便更容易地抑制警告:

#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 :(得分:208)

我对此的猜测是这样的:由于编译器不知道选择器,ARC无法强制执行适当的内存管理。

事实上,有时候内存管理通过特定约定与方法名称相关联。具体来说,我正在考虑便利构造函数 make 方法;前者按惯例返回自动释放的对象;后者是保留的对象。约定基于选择器的名称,因此如果编译器不知道选择器,则它无法强制执行适当的内存管理规则。

如果这是正确的,我认为您可以安全地使用您的代码,只要您确保内存管理的一切正常(例如,您的方法不会返回它们分配的对象)。

答案 3 :(得分:120)

在项目构建设置中,在其他警告标志WARNING_CFLAGS)下,添加
-Wno-arc-performSelector-leaks

现在只需确保您调用的选择器不会导致您的对象被保留或复制。

答案 4 :(得分:111)

作为解决方法,直到编译器允许覆盖警告,您可以使用运行时

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

而不是

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

您必须

#import <objc/message.h>

答案 5 :(得分:88)

要仅在带有执行选择器的文件中忽略错误,请按如下方式添加#pragma:

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

这会忽略此行上的警告,但在整个项目的其余部分仍然允许它。

答案 6 :(得分:68)

奇怪但真实:如果可以接受(即结果无效且您不介意让runloop循环一次),请添加延迟,即使该值为零:

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

这会删除警告,大概是因为它会向编译器保证不会返回任何对象并且以某种方式管理不当。

答案 7 :(得分:34)

这是基于上面给出的答案的更新宏。这个应该允许你使用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]
);

答案 8 :(得分:31)

此代码不涉及编译器标志或直接运行时调用:

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不同,这将适用于任何方法。

答案 9 :(得分:20)

嗯,这里有很多答案,但由于这有点不同,结合几个答案,我以为我会把它放进去。我正在使用一个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://stackoverflow.com/questions/7017281/performselector-may-cause-a-leak-because-its-selector-is-unknown

- (void) checkSelector:(SEL)aSelector {
    // See http://stackoverflow.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

答案 10 :(得分:16)

为了后人的缘故,我决定把帽子戴上戒指:)

最近我看到越来越多的重组远离target / selector范例,支持协议,块等等。但是,有一个直接替换对于performSelector,我现在已经使用了几次:

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

这些似乎是performSelector的干净,ARC安全且几乎完全相同的替代品,而不必使用objc_msgSend()

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

答案 11 :(得分:15)

Matt Galloway对this thread的回答解释了原因:

  

请考虑以下事项:

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

现在,ARC如何知道第一个返回一个保留计数为1但第二个保留计数的对象   返回一个自动释放的对象?

如果忽略返回值,通常可以安全地抑制警告。如果你真的需要从performSelector获取一个保留对象,我不确定最佳实践是什么 - 除了“不要那样做”。

答案 12 :(得分:14)

@ c-road提供了问题描述here的正确链接。下面你可以看到我的例子,当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返回保留的对象。

如果您运行内存泄漏工具,则可以看到以下图片: enter image description here ......在任何其他情况下都没有内存泄漏: enter image description here

答案 13 :(得分:6)

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

答案 14 :(得分:5)

不要禁止警告!

对于修补编译器,有不少于 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];

答案 15 :(得分:4)

因为您使用的是ARC,所以必须使用iOS 4.0或更高版本。这意味着你可以使用块。如果不是记住选择器来执行,而是采取阻止,ARC将能够更好地跟踪实际发生的情况,并且您不必冒着意外引入内存泄漏的风险。

答案 16 :(得分:2)

而不是使用块方法,这给了我一些问题:

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

我将使用NSInvocation,如下所示:

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

答案 17 :(得分:1)

如果您不需要传递任何参数,则一个简单的解决方法是使用valueForKeyPath。这甚至可以在Class对象上实现。

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

答案 18 :(得分:-2)

您也可以在这里使用协议。所以,创建一个像这样的协议:

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

在需要调用选择器的类中,您有一个@property。

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

如果需要在MyObject实例中调用@selector(doSomethingWithObject:),请执行以下操作:

[self.source doSomethingWithObject:object];