小编典典

performSelector 可能会导致泄漏,因为它的选择器是未知的

all

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

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

这就是我正在做的事情:

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

为什么我会收到此警告?我知道编译器无法检查选择器是否存在,但为什么会导致泄漏?以及如何更改我的代码以便不再收到此警告?


阅读 113

收藏
2022-02-25

共1个答案

小编典典

解决方案

编译器对此发出警告是有原因的。这个警告很少被忽略,而且很容易解决。这是如何做:

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 函数指针。所有NSObjects
都响应methodForSelector:,但您也可以class_getMethodImplementation在 Objective-C
运行时中使用(如果您只有协议引用,则很有用,例如id<SomeProto>)。这些函数指针称为IMPs,是简单的typedefed 函数指针 (
id (*IMP)(id, SEL, ...)) 1。这可能接近方法的实际方法签名,但并不总是完全匹配。

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

最后,调用函数指针2。

复杂示例

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

  1. 忽略非对象类型(void,int等)
  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在 C 中调用NULL函数是不安全的。用于检查控制器是否存在的守卫确保我们有一个对象。因此,我们知道我们会得到一个IMPfrom
methodForSelector:(虽然它可能是_objc_msgForward,进入消息转发系统)。基本上,有了守卫,我们就知道我们有一个函数可以调用。

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

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

2022-02-25