我得到ARC编译器的以下警告:

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

这是我正在做的:

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

为什么我得到这个警告?我理解编译器不能检查选择器是否存在,但为什么会导致泄漏?我怎样才能改变我的代码,让我不再得到这个警告呢?


当前回答

奇怪但事实是:如果可以接受(即result为void并且你不介意让runloop循环一次),添加一个延迟,即使这是零:

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

这删除了警告,大概是因为它向编译器保证没有对象可以返回,并且在某种程度上管理不当。

其他回答

我的猜测是这样的:因为选择器对编译器是未知的,ARC不能强制执行适当的内存管理。

事实上,有时内存管理通过特定的约定与方法的名称绑定在一起。具体来说,我在考虑方便构造函数和make方法;前者按照约定返回一个自动释放的对象;后者是一个保留对象。该约定基于选择器的名称,因此如果编译器不知道选择器,那么它就不能强制执行正确的内存管理规则。

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

不要压制警告!

有不少于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主线程]。

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

解决方案

编译器对此发出警告是有原因的。这种警告被忽略的情况非常罕见,而且很容易解决。方法如下:

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

解释

What's going on here is you're asking the controller for the C function pointer for the method corresponding to the controller. All NSObjects respond to methodForSelector:, but you can also use class_getMethodImplementation in the Objective-C runtime (useful if you only have a protocol reference, like id<SomeProto>). These function pointers are called IMPs, and are simple typedefed function pointers (id (*IMP)(id, SEL, ...))1. This may be close to the actual method signature of the method, but will not always match exactly.

一旦您有了IMP,您需要将它转换为一个函数指针,该函数指针包含ARC需要的所有细节(包括每个Objective-C方法调用的两个隐式隐藏参数self和_cmd)。这是在第三行中处理的(右边的(void *)只是告诉编译器你知道你在做什么,并且不生成警告,因为指针类型不匹配)。

最后,调用函数pointer2。

复杂的例子

当选择器接受参数或返回值时,你必须稍微改变一下:

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通常从你正在使用的对象类型的头文件中获取这些信息

ARC实际上只考虑4件事作为返回值

忽略非对象类型(void, int等) 保留对象值,然后在不再使用时释放(标准假设) 当不再使用时释放新的对象值(init/ copy族中的方法或带有ns_returns_retained属性的方法) 什么都不做,并假设返回的对象值在本地范围内有效(直到最内部的释放池被清空,属性为ns_returns_autoreleased)

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

For selectors you're trying to call that return void or other non-objects, you could enable compiler features to ignore the warning, but it may be dangerous. I've seen Clang go through a few iterations of how it handles return values that aren't assigned to local variables. There's no reason that with ARC enabled that it can't retain and release the object value that's returned from methodForSelector: even though you don't want to use it. From the compiler's perspective, it is an object after all. That means that if the method you're calling, someMethod, is returning a non object (including void), you could end up with a garbage pointer value being retained/released and crash.

附加参数

One consideration is that this is the same warning will occur with performSelector:withObject: and you could run into similar problems with not declaring how that method consumes parameters. ARC allows for declaring consumed parameters, and if the method consumes the parameter, you'll probably eventually send a message to a zombie and crash. There are ways to work around this with bridged casting, but really it'd be better to simply use the IMP and function pointer methodology above. Since consumed parameters are rarely an issue, this isn't likely to come up.

静态选择器

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

[_controller performSelector:@selector(someMethod)];

这样做的原因是因为编译器实际上能够在编译期间记录关于选择器和对象的所有信息。它不需要对任何事情做任何假设。(一年前我查看了资料来源,但现在没有参考资料。)

抑制

在试图思考一种情况下,抑制这个警告将是必要的,良好的代码设计,我是空白的。如果有人有过必要关闭此警告的经历,请分享(以上并不能正确处理事情)。

More

也可以构建一个NSMethodInvocation来处理这个,但是这样做需要更多的输入,也更慢,所以没有什么理由这样做。

历史

当performSelector:方法家族第一次被添加到Objective-C时,ARC还不存在。在创建ARC时,苹果决定为这些方法生成一个警告,作为一种指导开发人员使用其他方法来显式定义通过命名选择器发送任意消息时应该如何处理内存的方式。在Objective-C中,开发人员可以通过对原始函数指针使用C风格强制转换来实现这一点。

随着Swift的引入,苹果已经将performSelector:方法家族记录为“固有的不安全”,并且它们不适用于Swift。

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

早期版本的Objective-C允许performSelector:(手动内存管理) Objective-C使用ARC警告使用performSelector: Swift不能访问performSelector:并将这些方法记录为“固有的不安全”

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


所有Objective-C方法都有两个隐藏参数,self和_cmd,当你调用一个方法时,它们会被隐式添加。

2在c语言中调用NULL函数是不安全的。守卫用来检查控制器的存在,以确保我们有一个对象。因此,我们知道我们将从methodForSelector获得一个IMP(尽管它可能是_objc_msgForward,进入消息转发系统)。基本上,警卫就位后,我们就知道有一个函数要调用。

实际上,如果将你的对象声明为id,并且你没有导入所有的头文件,它可能会得到错误的信息。最终可能导致编译器认为正常的代码崩溃。这种情况非常罕见,但也有可能发生。通常你只会得到一个警告,说它不知道从两个方法签名中选择哪一个。

4关于保留返回值和未保留返回值的详细信息,请参见ARC参考。

@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返回retain object。

如果你运行内存泄漏工具,你可以看到下面的图片: ...在其他任何情况下都没有内存泄漏:

为了子孙后代着想,我决定参加竞选:)

最近,我看到越来越多的重构偏离了目标/选择器范式,而倾向于协议、块等。然而,我已经使用过几次的performSelector有一个替代物:

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

这些似乎是一个干净的,弧安全的,和几乎相同的替代performSelector没有太多的objc_msgSend()。

不过,我不知道iOS上是否有类似的功能。