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,运行时需要知道如何处理您所调用的方法的结果。 结果可能是任何东西: void
, int
, char
, NSString *
, id
等等。ARC通常从你正在使用的对象types的头部获取这些信息。 3
ARC确实只有4件事情会考虑返回值: 4
- 忽略非对象types(
void
,int
等) - 保留对象值,然后在不再使用时释放(标准假设)
- 不再使用时释放新的对象值(
init
/copy
系列中的方法或ns_returns_retained
属性) - 不要做任何事情,并假设返回的对象值将在本地范围内有效(直到最内层的发布池被耗尽,归因于
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。
随着时间的推移,我们看到了这样的进展
- 早期版本的Objective-C允许执行select器(手动内存pipe理)
- Objective-C与ARC警告使用
performSelector:
- 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
范例,有利于诸如协议,块等事情。但是,现在有一个performSelector
replaceperformSelector
,现在我已经使用了几次:
[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];