ARC编译器收到以下警告:
"performSelector may cause a leak because its selector is unknown".
这是我在做什么:
[_controller performSelector:NSSelectorFromString(@"someMethod")];
为什么会收到此警告?我知道编译器无法检查选择器是否存在,但是为什么会导致泄漏?以及如何更改我的代码,以便不再收到此警告?
ARC编译器收到以下警告:
"performSelector may cause a leak because its selector is unknown".
这是我在做什么:
[_controller performSelector:NSSelectorFromString(@"someMethod")];
为什么会收到此警告?我知道编译器无法检查选择器是否存在,但是为什么会导致泄漏?以及如何更改我的代码,以便不再收到此警告?
Answers:
编译器出于某种原因对此发出警告。很少会忽略此警告,并且很容易解决。就是这样:
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:
,但是您也可以class_getMethodImplementation
在Objective-C运行时中使用(如果仅具有协议参考,则很有用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,运行时需要知道如何处理所调用方法的结果。其结果可能是什么:void
,int
,char
,NSString *
,id
,等ARC通常会从您正在使用的对象类型的报头信息。3
ARC实际上只考虑4个返回值:4
void
,int
等)init
/ copy
系列中的方法或具有的方法ns_returns_retained
)ns_returns_autoreleased
)对的调用methodForSelector:
假定所调用方法的返回值是一个对象,但不保留/释放该对象。因此,如果应该按照上面的#3释放对象,那么最终可能会导致泄漏(即,您调用的方法将返回一个新对象)。
对于尝试调用该返回值void
或其他非对象的选择器,可以使编译器功能忽略该警告,但这可能很危险。我已经看到Clang对它如何处理未分配给局部变量的返回值进行了几次迭代。启用ARC并没有理由methodForSelector:
即使您不想使用它也无法保留和释放从中返回的对象值。从编译器的角度来看,它毕竟是一个对象。这意味着,如果您正在调用的方法someMethod
返回一个非对象(包括void
),则最终可能会导致垃圾指针值被保留/释放并崩溃。
一个考虑因素是,这将发生相同的警告,performSelector:withObject:
并且您可能在未声明该方法如何使用参数的情况下遇到类似的问题。ARC允许声明消耗的参数,如果该方法使用了该参数,您最终可能会向僵尸发送消息并崩溃。有多种方法可以解决桥接转换问题,但实际上最好只使用IMP
上面的and函数指针方法。由于消耗的参数很少出现问题,因此不太可能出现。
有趣的是,编译器不会抱怨静态声明的选择器:
[_controller performSelector:@selector(someMethod)];
这样做的原因是因为编译器实际上能够在编译期间记录有关选择器和对象的所有信息。它不需要对任何事情做任何假设。(我大约一年前通过查看源进行了检查,但目前没有参考。)
在试图考虑必须取消此警告和良好的代码设计的情况下,我显得空白。如果有人曾有过必须消除此警告的经验(并且上述内容无法正确处理),请与他人分享。
可能也可以构建一个NSMethodInvocation
来处理此问题,但是这样做需要更多的键入操作,而且输入速度也较慢,因此没有理由这样做。
当这一performSelector:
系列方法首次添加到Objective-C时,ARC不存在。在创建ARC时,Apple决定应为这些方法生成警告,以指导开发人员尝试使用其他方法来明确定义在通过命名选择器发送任意消息时应如何处理内存。在Objective-C中,开发人员可以通过对原始函数指针使用C样式强制转换来实现此目的。
随着Swift的推出,Apple 已将这一performSelector:
系列方法记录为“本质上不安全”,并且Swift无法使用它们。
随着时间的流逝,我们看到了这种进展:
performSelector:
(手动内存管理)performSelector:
performSelector:
并将这些方法记录为“本质上不安全”但是,基于命名选择器发送消息的想法并不是“本质上不安全”的功能。这个想法已经在Objective-C和许多其他编程语言中成功使用了很长时间。
1所有的Objective-C方法都有两个隐藏的参数,self
并且_cmd
在调用一个方法时会隐式添加这些参数。
2NULL
在C语言中调用函数并不安全。用于检查控制器是否存在的防护措施可确保我们有一个对象。因此,我们知道我们将从中获取IMP
信息methodForSelector:
(尽管可能来自_objc_msgForward
,进入消息转发系统)。基本上,有了后卫,我们知道我们要调用一个函数。
3实际上,如果将对象声明为as id
并且未导入所有标头,则可能会得到错误的信息。您可能最终会崩溃,导致编译器认为正常的代码崩溃。这是非常罕见的,但有可能发生。通常,您会收到一条警告,提示它不知道从两种方法签名中选择哪一种。
performSelector:
未以这种方式实现方法。它们具有严格的方法签名(return id
,采用1或2 id
s),因此不需要处理原始类型。
Cannot initialize a variable of type 'CGRect (*)(__strong id, SEL, CGRect, UIView *__strong)' with an rvalue of type 'void *'
使用最新的Xcode时,“复杂示例”给出错误。(5.1.1)我仍然学到了很多东西!
void (*func)(id, SEL) = (void *)imp;
无法编译,我将其替换为void (*func)(id, SEL) = (void (*)(id, SEL))imp;
void (*func)(id, SEL) = (void *)imp;
为<…> = (void (*))imp;
或<…> = (void (*) (id, SEL))imp;
在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]
);
pop
和push
wp-pragma包裹起来会更干净,更安全。
我对此的猜测是:由于选择器对于编译器是未知的,因此ARC无法强制执行适当的内存管理。
实际上,有时内存管理通过特定的约定与方法的名称绑定在一起。具体来说,我在考虑便捷构造函数与make方法;前者按惯例返回一个自动释放的对象;后者是保留的对象。约定基于选择器的名称,因此,如果编译器不知道选择器,则它将无法强制执行适当的内存管理规则。
如果正确的话,我认为您可以安全地使用您的代码,前提是您要确保对内存管理的一切正常(例如,方法不返回它们分配的对象)。
__attribute
向每个方法显式添加一个解释其内存管理的需要。但这也使编译器无法正确处理此模式(该模式过去非常普遍,但近年来已被更强大的模式所取代)。
SEL
并根据情况分配不同的选择器了吗?要走的路,动态的语言……
作为一种解决方法,直到编译器允许覆盖警告为止,您可以使用运行时
objc_msgSend(_controller, NSSelectorFromString(@"someMethod"));
代替
[_controller performSelector:NSSelectorFromString(@"someMethod")];
你必须
#import <objc/message.h>
[_controller performSelector:NSSelectorFromString(@"someMethod")];
并且objc_msgSend(_controller, NSSelectorFromString(@"someMethod"));
不等同!看一下方法签名不匹配和Objective-C弱类型的一个大弱点,他们正在深入解释这个问题。
要仅使用Perform选择器忽略文件中的错误,请添加#pragma,如下所示:
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
这将忽略此行上的警告,但在整个项目的其余部分仍将允许它。
#pragma clang diagnostic warning "-Warc-performSelector-leaks"
。我知道如果我关闭了警告,我希望尽快将其重新打开,因此我不会无意间让另一个意外的警告消失。这不太可能是一个问题,但是每当我关闭警告时,这只是我的做法。
#pragma clang diagnostic warning push
在进行任何更改之前和#pragma clang diagnostic warning pop
恢复以前的状态来恢复以前的编译器配置状态。如果您要关闭负载并且不想在代码中有很多重新启用编译指示行,则很有用。
奇怪但正确:如果可以接受(即结果为空,并且您不介意让运行循环循环一次),请添加一个延迟,即使该延迟为零:
[_controller performSelector:NSSelectorFromString(@"someMethod")
withObject:nil
afterDelay:0];
这消除了警告,大概是因为它使编译器确信没有对象可以返回并且以某种方式进行了错误管理。
这是基于上面给出的答案的更新的宏。该代码应该允许您即使使用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]
);
return
不必在宏内;return SUPPRESS_PERFORM_SELECTOR_LEAK_WARNING([_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类别,该类别检查以确保选择器返回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
为了后代的缘故,我决定将帽子戴上戒指:)
最近,我已经看到越来越多的结构从target
/ selector
范式中进行了重组,转而使用诸如协议,块等之类的东西。但是,有一种直接替代品performSelector
,我已经使用了几次:
[NSApp sendAction: NSSelectorFromString(@"someMethod") to: _controller from: nil];
这些似乎是一种干净的,ARC安全的并且几乎相同的替代品,performSelector
而不必太过注意objc_msgSend()
。
虽然,我不知道iOS上是否有类似产品。
[[UIApplication sharedApplication] sendAction: to: from: forEvent:]
。我曾经看过它,但是在您的域或服务中间使用与UI相关的类来进行动态调用有点尴尬。尽管如此,谢谢您!
id
从-performSelector:...
to:
为零,否则它不会“走上响应者链”,否则不会为零。它只是直接进入目标对象,无需事先检查。因此,没有“更多的开销”。这不是一个很好的解决方案,但是您给出的原因不是原因。:)
马特·加洛韦(Matt Galloway)在此主题上的回答解释了原因:
考虑以下:
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返回保留的对象。
如果您将运行“内存泄漏工具”,则可以看到以下图片: ...并且在任何其他情况下都没有内存泄漏:
为了使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];
performSelectorOnMainThread
方法不是使警告静音的好方法,并且会产生副作用。(它不能解决内存泄漏问题),额外功能#clang diagnostic ignored
可以非常明确地显式抑制警告。
- (void)
方法执行选择器是真正的问题。
而不是使用块方法,这给了我一些问题:
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];
}
您也可以在此处使用协议。因此,创建如下协议:
@protocol MyProtocol
-(void)doSomethingWithObject:(id)object;
@end
在需要调用选择器的类中,您将拥有一个@property。
@interface MyObject
@property (strong) id<MyProtocol> source;
@end
当您需要调用@selector(doSomethingWithObject:)
MyObject的实例时,请执行以下操作:
[self.source doSomethingWithObject:object];