performSelector可能会导致泄漏,因为它的select器是未知的

ARC编译器收到以下警告:

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

这是我正在做的事情:

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

为什么我得到这个警告? 我知道编译器不能检查select器是否存在,但为什么会导致泄漏? 我怎样才能改变我的代码,使我不再得到这个警告?

编译器正在警告这个原因。 这个警告应该被忽略,而且很容易解决。 就是这样:

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

一旦你有了IMP ,你需要把它转换成一个函数指针,该指针包含ARC需要的所有细节(包括每个Objective-C方法调用的两个隐含的隐含参数self_cmd )。 这是在第三行处理的(void *)右边的(void *)只是告诉编译器,你知道你在做什么,不会因为指针types不匹配而产生警告)。

最后,你调用函数指针2

复杂的例子

当select器使用参数或返回一个值时,你将不得不改变一些东西:

 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通常从你正在使用的对象types的头部获取这些信息。 3

ARC确实只有4件事情会考虑返回值: 4

  1. 忽略非对象types( voidint等)
  2. 保留对象值,然后在不再使用时释放(标准假设)
  3. 不再使用时释放新的对象值( init / copy系列中的方法或ns_returns_retained属性)
  4. 不要做任何事情,并假设返回的对象值将在本地范围内有效(直到最内层的发布池被耗尽,归因于ns_returns_autoreleased

methodForSelector:的调用假定它调用的方法的返回值是一个对象,但不保留/释放它。 所以如果你的对象应该像上面#3那样被释放(也就是说你调用的方法返回一个新的对象),那么你最终可能会创build一个泄漏。

对于试图调用返回void或其他非对象的select器,可以启用编译器function来忽略警告,但这可能是危险的。 我已经看到了Clang如何处理未分配给局部variables的返回值的迭代。 没有理由,启用ARC,它不能保留和释放从methodForSelector:返回的对象值methodForSelector:即使你不想使用它。 从编译器的angular度来看,它毕竟是一个对象。 这意味着如果您调用的方法someMethod正在返回一个非对象(包括void ),则最终可能会保留/释放垃圾指针值并导致崩溃。

额外的参数

一个需要注意的是,这与performSelector:withObject:会发生同样的警告,并且可能会遇到类似的问题,而不会声明该方法如何消耗参数。 ARC允许声明使用的参数 ,如果方法使用参数,你最终可能会发送消息给僵尸和崩溃。 有一些方法可以通过桥接模式来解决这个问题,但是真的使用上面的IMP和函数指针方法会更好。 由于消耗的参数很less成为问题,所以不太可能出现。

静态select器

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

 [_controller performSelector:@selector(someMethod)]; 

这是因为编译器实际上能够在编译期间logging有关select器和对象的所有信息。 不需要对任何事情做出任何假设。 (通过查看源代码,我在一年前查了一下,但现在没有参考。)

抑制

在试图想要抑制这个警告是必要的和好的代码devise的情况下,我会空白。 有人请分享,如果他们有经验,沉默这个警告是必要的(上面没有妥善处理的事情)。

更多

也可以build立一个NSMethodInvocation来处理这个,但是这样做需要更多的input,而且速度也更慢,所以没有什么理由去做。

历史

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

随着Swift的推出,苹果已经将performSelector:方法族logging为“固有的不安全”,并且它们不适用于Swift。

随着时间的推移,我们看到了这样的进展

  1. 早期版本的Objective-C允许执行select器(手动内存pipe理)
  2. Objective-C与ARC警告使用performSelector:
  3. Swift没有权限执行select器performSelector:并将这些方法logging为“固有的不安全”

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


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

2调用一个NULL函数在C中是不安全的。用来检查控制器存在的警卫确保我们有一个对象。 因此,我们知道我们将从methodForSelector:获得一个IMP (虽然它可能是_objc_msgForward ,进入消息转发系统)。 基本上,守卫到位,我们知道我们有一个function来打电话。

3实际上,如果声明你的对象是id而你没有导入所有的头文件,它可能会得到错误的信息。 编译器认为是好的,你可能会在代码中崩溃。 这是非常罕见的,但可能发生。 通常你会得到一个警告,它不知道两个方法签名中哪一个可以select。

4有关保留的返回值和未保留的返回值 ,请参阅ARC参考以获取更多详细信息。

在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 

如果你在几个地方得到了这个错误,并且想要使用Cmacros系统来隐藏编译指示,你可以定义一个macros来更容易地抑制这个警告:

 #define SuppressPerformSelectorLeakWarning(Stuff) \ do { \ _Pragma("clang diagnostic push") \ _Pragma("clang diagnostic ignored \"-Warc-performSelector-leaks\"") \ Stuff; \ _Pragma("clang diagnostic pop") \ } while (0) 

你可以像这样使用macros:

 SuppressPerformSelectorLeakWarning( [_target performSelector:_action withObject:self] ); 

如果你需要执行的消息的结果,你可以这样做:

 id result; SuppressPerformSelectorLeakWarning( result = [_target performSelector:_action withObject:self] ); 

我的猜测是这样的:由于编译器不知道select器,所以ARC不能执行正确的内存pipe理。

实际上,有些时候内存pipe理是通过特定的约定与方法的名字联系在一起的。 具体来说,我正在考虑方便的构造函数制作方法; 前者按惯例返还自动放弃的物品; 后者是保留的对象。 约定是基于select器的名字,所以如果编译器不知道select器,那么它就不能执行正确的内存pipe理规则。

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

在您的项目“ 生成设置”下的其他警告标志WARNING_CFLAGS )下,添加
-Wno-arc-performSelector-leaks

现在只要确保您所调用的select器不会导致您的对象被保留或复制。

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

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

代替

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

你必须

 #import <objc/message.h> 

要仅使用执行select器忽略文件中的错误,请按如下所示添加#pragma:

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

这将忽略这一行的警告,但仍然允许在整个项目的其余部分。

奇怪但是真实的:如果可以接受(即结果是无效的,你不介意让runloop循环一次),加一个延迟,即使这是零:

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

这消除了警告,大概是因为它保证编译器,没有对象可以返回,并以某种方式pipe理不善。

这是根据上面给出的答案更新的macros。 这个应该允许你用一个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] ); 

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

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

好吧,这里有很多答案,但是因为这有点不同,结合了一些我以为我会把它的答案。我使用NSObject类别,检查,以确保select器返回无效,也抑制了编译器警告。

 #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 

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

最近我已经看到越来越多的重构远离target / selector范例,有利于诸如协议,块等事情。但是,现在有一个performSelectorreplaceperformSelector ,现在我已经使用了几次:

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

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

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

马特加洛韦在这个线程的答案解释了为什么:

考虑以下几点:

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

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

如果你忽略了返回值,似乎通常是安全的。 我不确定最好的做法是什么,如果你真的需要从performSelector获得保留的对象 – 除了“不这样做”。

@ 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返回保留的对象。

如果你运行内存泄漏工具,你可以看到下面的图片: 在这里输入图像描述 …在任何其他情况下都没有内存泄漏: 在这里输入图像描述

使斯科特汤普森的macros观更通用:

 // 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或更高版本。 这意味着你可以使用块。 如果不是记住select器来执行,而是采取了一个块,ARC将能够更好地跟踪实际正在发生的事情,并且不必冒意外引入内存泄漏的风险。

不要压制警告!

有不less于12个替代解决scheme来修补编译器。
当你在第一次实施的时候很聪明的时候,地球上的几乎没有工程师可以跟随你的脚步,而这个代码最终会被打破。

安全路线:

所有这些解决scheme都可以工作,与您原来的意图有一定程度的差异。 假设如果你愿意, 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]; 

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