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

我收到ARC编译器的以下警告:

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

这是我正在做的:

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

为什么我会收到这个警告?我知道编译器无法检查选择器是否存在,但为什么会导致泄漏?我如何更改我的代码,这样我就不会再收到这个警告了?

187818 次浏览

我的猜测是:由于编译器不知道选择器,ARC无法强制执行正确的内存管理。

事实上,有时内存管理通过特定的约定与方法的名称联系在一起。具体来说,我在想便利构造函数使方法;前者按照约定返回一个自动释放的对象;后者是一个保留的对象。该约定基于选择器的名称,所以如果编译器不知道选择器,那么它就不能强制执行正确的内存管理规则。

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

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

您需要标题:

#import <objc/message.h>

然后试试下面:

// For strict compilers.((id(*)(id,SEL))objc_msgSend)(_controller, sel_getUid("someMethod"));

// Old answer's code:objc_msgSend(_controller, NSSelectorFromString(@"someMethod"));

而不是:

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

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

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

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

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

要仅在具有执行选择器的文件中忽略错误,请添加#pragma,如下所示:

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

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

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

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

为了子孙后代,我决定把我的帽子扔进戒指:)

最近,我看到越来越多的重组远离target/selector范式,支持协议、块等东西。然而,有一个我现在已经使用过几次的performSelector的替代方案:

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

这些似乎是一个干净的、ARC安全的、几乎相同的performSelector替代品,而不必与objc_msgSend()有太多的关系。

虽然,我不知道是否有一个模拟iOS。

Matt Galloway对这个线程的回答解释了原因:

考虑以下几点:

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

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

如果您忽略了返回值,那么抑制警告似乎通常是安全的。如果您真的需要从performSelector获取保留对象——除了“不要这样做”,我不确定最佳做法是什么。

奇怪但正确:如果可以接受(即结果为空,并且您不介意让runloop循环一次),请添加延迟,即使延迟为零:

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

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

@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;}

在我的例子中,导致内存泄漏的唯一方法是CopyDummyWith Leak。原因是ARC不知道,CopySelector返回保留的对象。

如果您运行内存泄漏工具,您可以看到以下图片:enter image description here…并且在任何其他情况下都没有内存泄漏:在此处输入图片描述

这是一个基于上面给出的答案的更新宏。这个应该允许您包装代码,即使使用返回语句。

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

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

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

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

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

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

[self.source doSomethingWithObject:object];

解决方案

编译器对此发出警告是有原因的。这个警告很少被忽略,而且很容易解决。以下是方法:

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>)。这些函数指针被称为IMPs,是简单的typedefed函数指针(id (*IMP)(id, SEL, ...)1。这可能接近方法的实际方法签名,但并不总是完全匹配。

一旦你有了IMP,你需要将它转换为一个函数指针,该指针包含ARC需要的所有细节(包括每个Objective-C方法调用的两个隐式隐藏参数self_cmd)。这在第三行中处理(右侧的(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:的调用假设它调用的方法的返回值是一个对象,但不保留/释放它。因此,如果您的对象应该像上面的#3那样被释放,您最终可能会创建一个泄漏(即,您正在调用的方法返回一个新对象)。

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

补充论点

一个考虑因素是,这与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. 带有ARC的Objective-C警告使用performSelector:
  3. Swift无法访问performSelector:并将这些方法记录为“本质上不安全”

然而,基于命名选择器发送消息的想法并不是一个“本质上不安全”的特性。这个想法在Objective-C以及许多其他编程语言中已经成功使用了很长时间。


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

2在C中调用NULL函数是不安全的。用于检查控制器是否存在的守卫确保我们有一个对象。因此,我们知道我们将从methodForSelector:中获得IMP(尽管它可能是_objc_msgForward,进入消息转发系统)。基本上,有了守卫,我们知道我们有一个函数可以调用。

3实际上,如果将对象声明为id并且没有导入所有标头,它可能会获取错误的信息。你最终可能会在编译器认为没问题的代码中崩溃。这很少见,但也有可能发生。通常你只会收到一个警告,它不知道从两个方法签名中选择哪一个。

4有关详细信息,请参阅保留返回值未保留的返回值上的ARC参考。

好吧,这里有很多答案,但由于这有点不同,结合一些答案,我想我应该把它放进去。我使用了一个NSObject类别,它检查以确保选择器返回空,并抑制编译器警告。

#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-valueMethod 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

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

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

我将使用NSInvation,如下所示:

    -(void) sendSelectorToDelegate:(SEL) selector withSender:(UIButton *)button
if ([delegate respondsToSelector:selector]){NSMethodSignature * methodSignature = [[delegate class]instanceMethodSignatureForSelector:selector];NSInvocation * delegateInvocation = [NSInvocationinvocationWithMethodSignature:methodSignature];

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

为了使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];)

不要压制警告!

修补编译器的替代解决方案不少于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]];

安全路线,行为略有不同:

(见
使用任何线程代替[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];

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

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