performSelector may cause a leak because its selector is unknown
ARC编译器发出以下警告:
1 | "performSelector may cause a leak because its selector is unknown". |
我要做的是:
1 | [_controller performSelector:NSSelectorFromString(@"someMethod")]; |
为什么我会收到这个警告?我理解编译器无法检查选择器是否存在,但为什么会导致泄漏?如何更改我的代码,使我不再收到此警告?
解决方案
编译器对此发出警告是有原因的。很少有人会忽视这个警告,而且很容易解决。以下是如何:好的。
1 2 3 4 5 | if (!_controller) { return; } SEL selector = NSSelectorFromString(@"someMethod"); IMP imp = [_controller methodForSelector:selector]; void (*func)(id, SEL) = (void *)imp; func(_controller, selector); |
或者更简洁地说(虽然没有防护装置很难阅读和理解):好的。
1 2 | SEL selector = NSSelectorFromString(@"someMethod"); ((void (*)(id, SEL))[_controller methodForSelector:selector])(_controller, selector); |
解释
这里要做的是,您向控制器请求与控制器对应的方法的C函数指针。所有
一旦拥有了
最后,调用函数指针2。好的。复杂实例
当选择器接受参数或返回一个值时,您必须稍微改变一下:好的。
1 2 3 4 5 | 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,运行时需要知道如何处理所调用方法的结果。结果可能是任何东西:
对于返回值,ARC实际上只考虑4件事情:4好的。
对
对于试图调用返回
一个考虑因素是,这与
有趣的是,编译器不会抱怨静态声明的选择器:好的。
1 | [_controller performSelector:@selector(someMethod)]; |
这是因为编译器实际上能够在编译期间记录关于选择器和对象的所有信息。它不需要对任何事情做任何假设。(一年前我通过查看资料来源查看了这一点,但现在没有参考资料。)好的。抑制
在试图思考一种情况,在这种情况下抑制这个警告是必要的和良好的代码设计,我来这里空白。如果有人有必要停止此警告的经验,请与他们分享(而上述情况处理不当)。好的。更多
也可以建立一个
当
随着Swift的引入,苹果已经将
随着时间的推移,我们看到了这一进展:好的。
但是,基于命名选择器发送消息的想法并不是"固有的不安全"特性。这个想法已经在Objective-C以及许多其他编程语言中成功地使用了很长一段时间。好的。
1所有的objective-c方法都有两个隐藏的参数:
2调用
3实际上,如果将对象声明为
4有关更多详细信息,请参见有关保留返回值和未保留返回值的弧参考。好的。好啊。
在xcode 4.2中的llvm 3.0编译器中,可以按如下方式取消警告:
1 2 3 4 | #pragma clang diagnostic push #pragma clang diagnostic ignored"-Warc-performSelector-leaks" [self.ticketTarget performSelector: self.ticketAction withObject: self]; #pragma clang diagnostic pop |
如果在多个地方出现错误,并且希望使用C宏系统隐藏pragma,则可以定义一个宏,以便更容易地抑制警告:
1 2 3 4 5 6 7 | #define SuppressPerformSelectorLeakWarning(Stuff) \ do { \ _Pragma("clang diagnostic push") \ _Pragma("clang diagnostic ignored "-Warc-performSelector-leaks"") \ Stuff; \ _Pragma("clang diagnostic pop") \ } while (0) |
可以这样使用宏:
1 2 3 | SuppressPerformSelectorLeakWarning( [_target performSelector:_action withObject:self] ); |
如果需要执行消息的结果,可以执行以下操作:
1 2 3 4 | id result; SuppressPerformSelectorLeakWarning( result = [_target performSelector:_action withObject:self] ); |
我的猜测是:由于编译器不知道选择器,所以ARC不能强制执行适当的内存管理。
实际上,有时内存管理通过特定的约定与方法的名称相关联。具体来说,我考虑的是方便的构造函数与make方法;前者按约定返回一个自动释放的对象;后者是保留的对象。该约定基于选择器的名称,因此如果编译器不知道选择器,那么它就无法强制执行正确的内存管理规则。
如果这是正确的,我认为您可以安全地使用您的代码,前提是您确保内存管理方面的一切都正常(例如,您的方法不返回它们分配的对象)。
在项目生成设置中,在其他警告标志(
现在,只要确保正在调用的选择器不会导致对象被保留或复制。
在编译器允许重写警告之前,您可以使用运行时
1 | objc_msgSend(_controller, NSSelectorFromString(@"someMethod")); |
而不是
1 | [_controller performSelector:NSSelectorFromString(@"someMethod")]; |
你得去ZZU1[2]
要仅忽略带有执行选择器的文件中的错误,请添加一个pragma,如下所示:
1 | #pragma clang diagnostic ignored"-Warc-performSelector-leaks" |
这将忽略这一行上的警告,但仍然允许它贯穿整个项目的其余部分。
奇怪但真实:如果可以接受(即结果是空的,您不介意让runloop循环一次),添加一个延迟,即使这是零:
1 2 3 | [_controller performSelector:NSSelectorFromString(@"someMethod") withObject:nil afterDelay:0]; |
这将删除警告,可能是因为它使编译器确信没有对象可以返回,并且以某种方式管理不当。
下面是基于上述答案的更新宏。这个应用程序应该允许您使用RETURN语句包装代码。
1 2 3 4 5 6 7 8 9 10 | #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] ); |
此代码不涉及编译器标志或直接运行时调用:
1 2 3 4 5 6 | SEL selector = @selector(zeroArgumentMethod); NSMethodSignature *methodSig = [[self class] instanceMethodSignatureForSelector:selector]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig]; [invocation setSelector:selector]; [invocation setTarget:self]; [invocation invoke]; |
嗯,这里有很多答案,但由于这有点不同,结合了一些答案,我想我会把它放进去。我使用的是nsObject类别,它检查以确保选择器返回void,并抑制编译器警告。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | #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 |
为了子孙后代,我决定把帽子扔进戒指里。)
最近,我看到越来越多的重组远离了
1 | [NSApp sendAction: NSSelectorFromString(@"someMethod") to: _controller from: nil]; |
这些似乎是一个干净的,电弧安全,几乎相同的替代品,为
不过,我不知道iOS上是否有可用的模拟设备。
Matt Galloway在这条线上的回答解释了原因:
Consider the following:
1
2 id anotherObject1 = [someObject performSelector:@selector(copy)];
id anotherObject2 = [someObject performSelector:@selector(giveMeAnotherNonRetainedObject)];Now, how can ARC know that the first returns an object with a retain count of 1 but the second
returns an object which is autoreleased?
如果忽略返回值,通常可以安全地抑制警告。如果您真的需要从PerformSelector中获取保留的对象——而不是"不要这样做",那么我不确定最佳实践是什么。
@C-Road在这里提供了问题描述的正确链接。下面您可以看到我的示例,当PerformSelector导致内存泄漏时。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | @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返回保留的对象。
如果要运行内存泄漏工具,可以看到以下图片:…在任何其他情况下都没有内存泄漏:
要使Scott Thompson的宏更通用:
1 2 3 4 5 6 7 8 9 | // 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") |
然后这样使用:
1 2 3 | MYSilenceWarning(-Warc-performSelector-leaks, [_target performSelector:_action withObject:self]; ) |
不要取消警告!
对编译器进行修补的替代解决方案不少于12种。虽然您在第一次实现时很聪明,但地球上很少有工程师能跟随您的脚步,而这段代码最终会被破坏。
安全路线:
所有这些解决方案都会起作用,与您最初的意图存在一定程度的差异。假设您希望,
安全路线,相同的概念行为:
1 2 3 4 5 6 | // 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]]; |
安全路线,行为稍有不同:
(见此回复)使用任何螺纹代替
1 2 3 4 5 6 7 8 9 10 11 12 | // 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]]; |
危险路线
需要某种类型的编译器静默,这必然会中断。请注意,目前,它确实在迅速突破。
1 2 3 4 | // AT YOUR OWN RISK [_controller performSelector:selector]; [_controller performSelector:selector withObject:anArgument]; [_controller performSelector:selector withObject:anArgument withObject:nil]; |
因为您使用的是ARC,所以必须使用iOS 4.0或更高版本。这意味着你可以使用积木。如果不是记住选择器来执行,而是选择一个块,ARC将能够更好地跟踪实际发生的事情,并且您不必冒意外引入内存泄漏的风险。
而不是使用块方法,这给了我一些问题:
1 2 | IMP imp = [_controller methodForSelector:selector]; void (*func)(id, SEL) = (void *)imp; |
我将使用nsinvocation,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | -(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]; } |
如果您不需要传递任何参数,一个简单的解决方法是使用
1 2 3 4 5 6 | NSString *colorName = @"brightPinkColor"; id uicolor = [UIColor class]; if ([uicolor respondsToSelector:NSSelectorFromString(colorName)]){ UIColor *brightPink = [uicolor valueForKeyPath:colorName]; ... } |
你也可以在这里使用协议。所以,创建一个这样的协议:
1 2 3 | @protocol MyProtocol -(void)doSomethingWithObject:(id)object; @end |
在需要调用选择器的类中,您有一个@property。
1 2 3 | @interface MyObject @property (strong) id<MyProtocol> source; @end |
当需要在MyObject实例中调用
1 | [self.source doSomethingWithObject:object]; |