题 performSelector可能导致泄漏,因为其选择器未知


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

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

这就是我正在做的事情:

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

为什么我会收到这个警告?我理解编译器无法检查选择器是否存在,但为什么会导致泄漏?我怎样才能更改我的代码,以便我不再收到此警告?


1190
2017-08-10 20:23


起源


变量的名称是动态的,它取决于很多其他的东西。有可能我称之为不存在的东西,但这不是问题。 - Eduardo Scoz
@matt为什么在对象上动态调用方法是不好的做法? NSSelectorFromString()的全部目的不是支持这种做法吗? - Eduardo Scoz
您应该/还可以在通过performSelector设置之前测试[_controller respondsToSelector:mySelector]: - mattacular
@mattacular希望我能投票:“那......是不好的做法。” - ctpenrose
如果您知道字符串是文字,只需使用@selector(),以便编译器可以告诉选择器名称是什么。如果您的实际代码使用在运行时构造或提供的字符串调用NSSelectorFromString(),则必须使用NSSelectorFromString()。 - Chris Page


答案:


出于某种原因,编译器会对此发出警告。这个警告应该被忽略,这很容易解决。就是这样:

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>)。调用这些函数指针 IMPs,很简单 typedefed函数指针(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,运行时需要知道如何处理您正在调用的方法的结果。结果可能是任何事情: voidintcharNSString *idARC通常从您正在使用的对象类型的标题中获取此信息。3

ARC确实只有4件事要考虑返回值:4

  1. 忽略非对象类型(voidint等)
  2. 保留对象值,然后在不再使用时释放(标准假设)
  3. 不再使用时释放新的对象值(中的方法) init/ copy 家庭或归因于 ns_returns_retained
  4. 什么也不做,并假设返回的对象值在本地范围内有效(直到内部大多数发布池被耗尽,归因于 ns_returns_autoreleased

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

对于选择器,您尝试调用该返回 void 或者其他非对象,您可以启用编译器功能来忽略警告,但这可能是危险的。我已经看到Clang经历了一些如何处理未分配给局部变量的返回值的迭代。启用ARC时没有理由不能保留和释放从中返回的对象值 methodForSelector:即使你不想使用它。从编译器的角度来看,它毕竟是一个对象。这意味着,如果您正在调用的方法, someMethod,正在返回一个非对象(包括 void),你最终可能会保留/释放垃圾指针值并崩溃。

附加参数

一个考虑是,这是同样的警告会发生 performSelector:withObject: 并且你可能遇到类似的问题而没有声明该方法如何使用参数。 ARC允许声明 消耗参数,如果该方法使用该参数,您最终可能会向僵尸发送消息并崩溃。有一些方法可以通过桥接铸造来解决这个问题,但实际上最好只使用它 IMP 以及上面的函数指针方法。由于消耗的参数很少成为问题,因此不太可能出现。

静态选择器

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

[_controller performSelector:@selector(someMethod)];

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

抑制

在试图考虑抑制此警告是必要的以及良好的代码设计的情况时,我发现空白。有人请分享,如果他们有经验,需要沉默这个警告(并且以上不能正确处理事情)。

更多

有可能建立一个 NSMethodInvocation 处理这个,但这样做需要更多的打字,也更慢,所以没有理由这样做。

历史

当。。。的时候 performSelector: 方法家族首先被添加到Objective-C,ARC不存在。在创建ARC时,Apple决定应为这些方法生成警告,以指导开发人员使用其他方法明确定义在通过命名选择器发送任意消息时应如何处理内存。在Objective-C中,开发人员可以通过在原始函数指针上使用C样式转换来实现此目的。

随着Swift,Apple的推出 记录了 该 performSelector: 作为“天生不安全”的方法系列,它们不适用于Swift。

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

  1. 早期版本的Objective-C允许 performSelector: (手动内存管理)
  2. ARC的Objective-C警告使用 performSelector:
  3. Swift无权访问 performSelector: 并将这些方法记录为“本质上不安全”

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


1 所有Objective-C方法都有两个隐藏的参数, self 和 _cmd 在调用方法时隐式添加的内容。

2打电话给 NULL 功能在C中是不安全的。用于检查控制器是否存在的防护装置确保我们有一个对象。因此,我们知道我们会得到一个 IMP 从 methodForSelector: (虽然可能是 _objc_msgForward,进入消息转发系统)。基本上,在守卫到位的情况下,我们知道我们有一个可以打电话的功能。

3 实际上,如果将对象声明为,它可能会得到错误的信息 id 而且你没有导入所有标题。你最终可能会遇到编译器认为没问题的代码崩溃。这种情况非常罕见,但可能会发生。通常,您只会收到一条警告,表示它不知道可以选择哪两种方法签名。

4 请参阅ARC参考 保留的返回值 和 未返回的返回值 更多细节。


1142
2017-11-18 21:44



@wbyoung如果你的代码解决了保留问题,我想知道为什么 performSelector: 方法没有这种方式实现。他们有严格的方法签名(返回 id拿一两个 ids),因此不需要处理原始类型。 - Tricertops
@Andy参数是根据方法原型的定义来处理的(它不会被保留/释放)。关注点主要基于返回类型。 - wbyoung
“复杂示例”给出了错误 Cannot initialize a variable of type 'CGRect (*)(__strong id, SEL, CGRect, UIView *__strong)' with an rvalue of type 'void *' 使用最新的Xcode时。 (5.1.1)尽管如此,我学到了很多东西! - Stan James
void (*func)(id, SEL) = (void *)imp; 不编译,我用它替换它 void (*func)(id, SEL) = (void (*)(id, SEL))imp; - Davyd
更改 void (*func)(id, SEL) = (void *)imp; 至 <…> = (void (*))imp; 要么 <…> = (void (*) (id, SEL))imp; - Isaak Osipovich Dunayevsky


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

1171
2017-10-28 19:30



当优化设置为None以外的任何值时,此方法可能导致内存泄漏。 - Eric
@Eric不,它不能,除非你调用有趣的方法,如“initSomething”或“newSomething”或“somethingCopy”。 - Andrey Tarantsov
@Julian这确实有效,但是会关闭整个文件的警告 - 你可能不需要或不想要它。用它来包装它 pop 和 push-pragma更清洁,更安全。 - Emil
所有这一切都是它使编译器沉默。这并没有解决问题。如果选择器不存在,那你几乎搞砸了。 - Andra Todorescu
这应该仅在被包装时使用 if ([_target respondsToSelector:_selector]) { 或类似的逻辑。 - Barry


我对此的猜测是这样的:由于编译器不知道选择器,ARC无法强制执行适当的内存管理。

实际上,有时候内存管理通过特定约定与方法的名称相关联。具体来说,我在考虑 便利施工人员 与 使 方法;前者按惯例返回自动释放的对象;后者是保留的对象。约定基于选择器的名称,因此如果编译器不知道选择器,则它无法强制执行适当的内存管理规则。

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


206
2017-08-10 20:43



谢谢你的回答,我会更多地了解这一点,看看发生了什么。关于如何绕过警告但让它消失的任何想法?我不愿意在我的代码中永远保留警告,以确保安全通话。 - Eduardo Scoz
所以我在他们的论坛上得到了苹果公司的确认,确实如此。他们将添加一个被遗忘的覆盖,以允许人们在将来的版本中禁用此警告。谢谢。 - Eduardo Scoz
这个答案提出了一些问题,比如ARC试图根据约定和方法名称确定何时释放某些内容,那么它是如何“引用计数”的呢?如果ARC假设代码遵循某种约定而不是实际跟踪引用而不管遵循什么约定,那么您描述的行为听起来只比完全随意更好。 - aroth
ARC在编译时自动添加保留和发布的过程。它不是垃圾收集(这就是为什么它如此快速和低开销)。它根本不是任意的。默认规则基于成熟的ObjC约定,这些约定已经持续应用了数十年。这样就无需显式添加 __attribute解释其内存管理的每种方法。但它也使编制者无法正确处理这种模式(这种模式曾经非常普遍,但近年来已被更强大的模式所取代)。 - Rob Napier
所以我们不能再有类型的ivar SEL 并根据情况分配不同的选择器?方式去,动态语言...... - Nicolas Miari


在你的项目中 构建设置,在 其他警告标志 (WARNING_CFLAGS),添加
-Wno-arc-performSelector-leaks

现在只需确保您调用的选择器不会导致保留或复制您的对象。


119
2017-10-31 13:57



请注意,您可以为特定文件而不是整个项目添加相同的标志。如果您查看Build Phases-> Compile Sources,您可以设置每个文件Compiler Flags(就像您想要从ARC中排除文件一样)。在我的项目中,只有一个文件应该以这种方式使用选择器,所以我只是将其排除并留下其他文件。 - Michael


作为解决方法,直到编译器允许覆盖警告,您可以使用运行时

objc_msgSend(_controller, NSSelectorFromString(@"someMethod"));

代替

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

你必须这样做

#import <objc/message.h>


110
2017-08-16 04:56



ARC承认Cocoa约定,然后根据这些约定添加保留和发布。由于C不遵循这些约定,因此ARC会强制您使用手动内存管理技术。如果您创建CF对象,则必须CFRelease()它。如果你dispatch_queue_create(),你必须dispatch_release()。最重要的是,如果要避免ARC警告,可以通过使用C对象和手动内存管理来避免它们。此外,您可以通过在该文件上使用-fno-objc-arc编译器标志来基于每个文件禁用ARC。 - jluckyiv
不是没有铸造,你不能。 Varargs与显式类型参数列表不同。它通常是巧合的,但我并不认为“巧合”是正确的。 - bbum
不要那样做, [_controller performSelector:NSSelectorFromString(@"someMethod")]; 和 objc_msgSend(_controller, NSSelectorFromString(@"someMethod")); 不等同!看一下 方法签名不匹配 和 Objective-C弱类型的一大弱点 他们正在深入解释这个问题。 - 0xced
@ 0xced在这种情况下,没关系。 objc_msgSend不会为任何在performSelector中正常工作的选择器或其变体创建方法签名不匹配,因为它们只将对象作为参数。只要你的所有参数都是指针(包括对象),双精度和NSInteger / long,并且你的返回类型是void,pointer或long,那么objc_msgSend将正常工作。 - Matt Gallagher


要仅使用执行选择器在文件中忽略错误,请按如下方式添加#pragma:

#pragma clang diagnostic ignored "-Warc-performSelector-leaks"

这会忽略此行上的警告,但在整个项目的其余部分仍然允许它。


87
2018-01-18 21:31



我知道你也可以在有问题的方法之后立即重新发出警告 #pragma clang diagnostic warning "-Warc-performSelector-leaks"。我知道如果我关闭警告,我想尽快将其重新打开,所以我不小心让另一个意外的警告失误。这不太可能是一个问题,但每当我关闭警告时,这只是我的做法。 - Rob
您还可以使用恢复以前的编译器配置状态 #pragma clang diagnostic warning push 在你做任何改变之前 #pragma clang diagnostic warning pop 恢复以前的状态。如果要关闭负载并且不希望在代码中重新启用pragma行,则非常有用。 - deanWombourne
它只会忽略以下行? - hfossli


奇怪但却是真的:如果可接受(即结果无效且你不介意让runloop循环一次),添加一个延迟,即使这是零:

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

这会删除警告,大概是因为它让编译器放心不能返回任何对象并且以某种方式管理不当。


67
2017-11-11 19:19



你知道这是否真的解决了相关的内存管理问题,或者它是否有相同的问题但是Xcode不够聪明,不能用这个代码警告你? - Aaron Brager
这在语义上是不一样的!使用performSelector:withObject:AfterDelay:将在下一次运行runloop时执行选择器。因此,此方法立即返回。 - Florian
@Florian当然不一样!读我的回答:我说 如果 可以接受,因为结果是无效的并且runloop循环。那就是 第一句话 我的回答。 - matt