协慌网

登录 贡献 社区

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

ARC 编译器收到以下警告:

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

这是我正在做的事情:

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

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

答案

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

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

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 s,是简单的typedef ed 函数指针( id (*IMP)(id, SEL, ...)1 。这可能接近方法的实际方法签名,但并不总是完全匹配。

一旦有了IMP ,就需要将它转换为包含 ARC 所需的所有细节的函数指针(包括每个 Objective-C 方法调用的两个隐式隐藏参数self_cmd )。这是在第三行处理的(void *)右侧的(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 *id等. ARC 通常从您正在使用的对象类型的标头中获取此信息。 3

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

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

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

对于您尝试调用返回void或其他非对象的选择器,您可以启用编译器功能来忽略警告,但这可能很危险。我已经看到 Clang 经历了一些如何处理未分配给局部变量的返回值的迭代。启用 ARC 时没有理由不保留和释放从methodForSelector:返回的对象值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在 C 中调用NULL函数是不安全的。用于检查控制器是否存在的防护确保我们有一个对象。因此,我们知道我们将从methodForSelector:获取IMP methodForSelector:尽管它可能是_objc_msgForward ,进入消息转发系统)。基本上,在守卫到位的情况下,我们知道我们有一个可以打电话的功能。

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

4有关更多详细信息,请参阅有关保留返回值和未返回返回值的 ARC 参考。

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

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

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