我收到 ARC 编译器的以下警告:
"performSelector may cause a leak because its selector is unknown".
这就是我正在做的事情:
[_controller performSelector:NSSelectorFromString(@"someMethod")];
为什么我会收到此警告?我知道编译器无法检查选择器是否存在,但为什么会导致泄漏?以及如何更改我的代码以便不再收到此警告?
解决方案
编译器对此发出警告是有原因的。这个警告很少被忽略,而且很容易解决。就是这样:
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 *)
只是告诉编译器您知道自己在做什么,并且由于指针类型不匹配而不会生成警告)。
最后,调用函数 pointer2。
复杂示例
When the selector takes arguments or returns a value, you'll have to change things a bit:
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 允许声明 consumed parameters,如果该方法使用参数,您最终可能会向僵尸发送消息并崩溃。有一些方法可以通过桥接转换来解决这个问题,但实际上最好简单地使用上面的 IMP
和函数指针方法。由于消耗的参数很少成为问题,因此不太可能出现。
静态选择器
有趣的是,编译器不会抱怨静态声明的选择器:
[_controller performSelector:@selector(someMethod)];
这样做的原因是因为编译器实际上能够在编译期间记录有关选择器和对象的所有信息。它不需要对任何事情做任何假设。 (我在一年前通过查看源代码检查了这一点,但现在没有参考。)
抑制
在尝试考虑抑制此警告是必要的和良好的代码设计的情况时,我会一头雾水。如果有人有过需要消除此警告的经验(并且上述内容无法正确处理),请分享。
更多的
也可以构建一个 NSMethodInvocation
来处理这个问题,但是这样做需要更多的输入并且速度也很慢,所以没有理由这样做。
历史
当 performSelector:
系列方法首次添加到 Objective-C 时,ARC 并不存在。在创建 ARC 时,Apple 决定应该为这些方法生成警告,以指导开发人员使用其他方式明确定义在通过命名选择器发送任意消息时应如何处理内存。在 Objective-C 中,开发人员可以通过在原始函数指针上使用 C 风格强制转换来做到这一点。
随着 Swift 的引入,Apple has documented performSelector:
系列方法“本质上是不安全的”,它们不适用于 Swift。
随着时间的推移,我们看到了这种进展:
Objective-C 的早期版本允许 performSelector:(手动内存管理)带有 ARC 的 Objective-C 警告使用 performSelector:Swift 无权访问 performSelector:并将这些方法记录为“本质上不安全”
然而,基于命名选择器发送消息的想法并不是“固有的不安全”特性。这个想法已经在 Objective-C 以及许多其他编程语言中成功使用了很长时间。
1 所有的 Objective-C 方法都有两个隐藏的参数,self
和 _cmd
,它们是在您调用方法时隐式添加的。
2 在 C 中调用 NULL
函数是不安全的。用于检查控制器是否存在的守卫确保我们有一个对象。因此,我们知道我们会从 methodForSelector:
获得一个 IMP
(尽管它可能是 _objc_msgForward
,进入消息转发系统)。基本上,有了守卫,我们就知道我们有一个函数可以调用。
3 实际上,如果将您的对象声明为 id
并且您没有导入所有标头,则它可能会获取错误信息。您最终可能会导致编译器认为没问题的代码崩溃。这是非常罕见的,但可能会发生。通常你只会得到一个警告,它不知道从两个方法签名中选择哪一个。
4 有关详细信息,请参阅 retained return values 和 unretained return values 上的 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
如果您在多个地方遇到错误,并且想使用 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
-pragma 包装它更干净、更安全。
if ([_target respondsToSelector:_selector]) {
或类似逻辑包装时使用。
我对此的猜测是:由于编译器不知道选择器,ARC 无法强制执行正确的内存管理。
事实上,有时内存管理通过特定约定与方法名称相关联。具体来说,我正在考虑便利构造函数与 make 方法;前者按惯例返回一个自动释放的对象;后者是保留对象。该约定基于选择器的名称,因此如果编译器不知道选择器,则它无法强制执行正确的内存管理规则。
如果这是正确的,我认为您可以安全地使用您的代码,前提是您确保内存管理一切正常(例如,您的方法不返回它们分配的对象)。
__attribute
的需要。但这也使得编译器无法正确处理这种模式(这种模式曾经非常普遍,但近年来已被更强大的模式所取代)。
SEL
类型的 ivar 并根据情况分配不同的选择器了吗?走的路,动态语言...
在您的项目Build Settings 中,在Other Warning Flags (WARNING_CFLAGS
) 下,添加
-Wno-arc-performSelector-leaks
现在只需确保您调用的选择器不会导致您的对象被保留或复制。
作为一种解决方法,直到编译器允许覆盖警告,您可以使用运行时。
你需要标题:
#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")];
[_controller performSelector:NSSelectorFromString(@"someMethod")];
和 objc_msgSend(_controller, NSSelectorFromString(@"someMethod"));
不等价!看看 Method Signature Mismatches 和 A big weakness in Objective-C's weak typing 他们正在深入解释问题。
要仅使用 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
恢复以前的状态。如果您要关闭负载并且不想在代码中包含大量重新启用的编译指示行,这很有用。
奇怪但真实:如果可以接受(即结果为 void 并且您不介意让 runloop 循环一次),则添加延迟,即使它为零:
[_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];
这些似乎是对 performSelector
的干净、ARC 安全且几乎相同的替代品,而无需对 objc_msgSend()
进行过多处理。
不过,我不知道 iOS 上是否有可用的模拟。
[[UIApplication sharedApplication] sendAction: to: from: forEvent:]
。我看过一次,但是在你的域或服务中间使用与 UI 相关的类只是为了进行动态调用有点尴尬。谢谢你包括这个!
-performSelector:...
的 id
时,它也不起作用
to:
为 nil,否则它不会“沿着响应者链向上走”,但事实并非如此。它只是直接到达目标对象而无需事先检查。所以没有“更多的开销”。这不是一个很好的解决方案,但你给出的理由不是理由。 :)
Matt Galloway 在 this thread 上的回答解释了原因:
考虑以下内容: id anotherObject1 = [someObject performSelector:@selector(copy)]; id anotherObject2 = [someObject performSelector:@selector(giveMeAnotherNonRetainedObject)];现在,ARC 怎么知道第一个返回一个保留计数为 1 的对象,而第二个返回一个自动释放的对象?
如果您忽略返回值,似乎通常可以安全地抑制警告。如果您确实需要从 performSelector 获取保留对象,我不确定最佳做法是什么——除了“不要那样做”。
@c-road 提供了问题描述 here 的正确链接。下面你可以看到我的示例,当 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 返回保留对象。
https://i.stack.imgur.com/iW1Po.png
为了使 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]];
安全路线,行为略有不同:
(参见 this 响应)
使用任何线程代替 [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)
方法上执行选择器是真正的问题。
因为您使用的是 ARC,所以您必须使用 iOS 4.0 或更高版本。这意味着您可以使用块。如果不是记住选择器来执行您而是执行一个块,ARC 将能够更好地跟踪实际发生的事情,并且您不必冒意外引入内存泄漏的风险。
self
时出现编译器警告(例如 ivar
而不是 self->ivar
)。
而不是使用块方法,这给了我一些问题:
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];
}
如果您不需要传递任何参数,一个简单的解决方法是使用 valueForKeyPath
。这甚至可以在 Class
对象上实现。
NSString *colorName = @"brightPinkColor";
id uicolor = [UIColor class];
if ([uicolor respondsToSelector:NSSelectorFromString(colorName)]){
UIColor *brightPink = [uicolor valueForKeyPath:colorName];
...
}
您也可以在此处使用协议。因此,创建一个像这样的协议:
@protocol MyProtocol
-(void)doSomethingWithObject:(id)object;
@end
在需要调用选择器的类中,您将拥有一个 @property。
@interface MyObject
@property (strong) id<MyProtocol> source;
@end
当您需要在 MyObject 的实例中调用 @selector(doSomethingWithObject:)
时,请执行以下操作:
[self.source doSomethingWithObject:object];
不定期副业成功案例分享
performSelector:
方法没有以这种方式实现。它们具有严格的方法签名(返回id
,取一或两个id
),因此不需要处理原始类型。Cannot initialize a variable of type 'CGRect (*)(__strong id, SEL, CGRect, UIView *__strong)' with an rvalue of type 'void *'
。 (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;