ios - performSelector可能造成泄漏, 因为它的选择器未知

  显示原文与译文双语对照的内容

我正在通过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);

说明

这里发生的是你正在请求控制器的函数指针,该指针对应于控制器的方法。 objective-c 运行时中的所有 NSObject 是对 methodForSelector: 作出响应,但你也可以使用 这些函数指针称为 IMP,并且是简单的typedef 函数指针( id (*IMP)(id, SEL,.. .) ) 1 这可能与方法的实际方法签名接近,但不总是匹配。

一旦你拥有了 IMP,你需要将它转换为一个函数指针,它包含所有需要( 包括这两个隐式隐藏参数 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 *id,等等 ARC通常从你正在使用的对象类型的头部获取这里信息。 3

对于返回值,ARC只考虑了 4个东西: 4

  1. 忽略non-object类型( voidint 等)
  2. 保留对象值,然后在不再使用时释放它( 标准假设)
  3. 当不再使用时释放新的对象值( init/copy 族中的方法或者具有 ns_returns_retained的属性)
  4. 不执行任何操作&假定返回的对象值在本地范围内有效( 直到内部大多数释放池被清空,用 ns_returns_autoreleased 进行属性化)

methodForSelector:的调用假定调用的方法的返回值是一个对象,但不保留/释放它。 因此,如果你的对象应该像在 #3 上那样释放,那么你可以创建一个泄漏。

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

附加参数

一个考虑是,这是与 performSelector:withObject: 相同的警告,你可以在不声明方法如何使用参数的情况下遇到类似的问题。 ARC允许声明消耗的参数,如果方法使用参数,你可能最终将消息发送给僵尸和崩溃。 有一些方法可以使用桥接铸造来解决这个问题,但是最好使用上面的IMP 和函数指针方法。 由于使用的参数很少是一个问题,所以不太可能出现。

静态选择器

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


[_controller performSelector:@selector(someMethod)];

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

抑制

在试图考虑抑制这个警告是必要的和良好的代码设计的情况下,我将得到空白。 如果有人遇到了关闭这里警告是必需的( 上面的内容并不正确处理),请共享。

更多

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

历史

首次将 performSelector: 家族添加到 objective-c 时,弧不存在。 在创建ARC时,苹果决定为这些方法生成警告,以指导开发人员使用其他方法在通过命名选择器发送任意消息时显式地定义内存。 在 objective-c 中,开发者可以通过在原始函数指针上使用C 风格的类型转换来实现。

随着 Swift的引入,苹果已经将 performSelector: 家族的方法"固有不安全"documented,它们对 Swift 不可用。

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

  1. 早期版本的objective-c 允许 performSelector: ( 手动内存管理)
  2. 带圆弧的objective-c 警告使用 performSelector:
  3. Swift 不能访问 performSelector: 并将这些方法作为"固有不安全""

但是,基于命名选择器发送消息的想法不是"固有不安全"特性。 这个想法在 objective-c 中已经成功使用了很长时间,还有许多其他编程语言。


1 所有 objective-c 方法都有两个隐藏参数 self_cmd,它们在调用方法时隐式添加。

2 调用 NULL 函数在中不安全。 用来检查控制器是否存在的守卫确保我们有一个对象。 在 methodForSelector: 因此,我们知道我们将得到一个 基本上,当守卫就位时,我们知道有一个函数可以调用。

3 实际上,如果将你的对象声明为 id,并且你没有导入所有的标题,那么它就可能得到错误的信息。 你可能会遇到编译器认为很好的代码崩溃。 这很罕见,但可能会发生。 通常情况下,你会得到一个警告,它不知道要选择哪种方法签名。

4 查看上的圆弧引用保留返回值unretained返回值for了解更多详细信息。

在LLVM编译器Xcode中 3.0 4.2可以禁止显示该警告如下:


#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Warc-performSelector-leaks"
 [self.ticketTarget performSelector: self.ticketAction withObject: self];
#pragma clang diagnostic pop

如果你在几个地方都有错误,你可以定义一个宏,使它的更容易抑制警告:


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

我的猜测是: 由于编译器未知,所以ARC不能强制执行适当的内存管理。

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

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

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

现在只要确定要调用的选择器,不会导致你的对象上要保留或者复制。

作为解决方法,直到编译器允许重写警告,你可以使用运行时

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

代替

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

你必须得

#import <objc/message.h>

要只在具有执行选择器的文件中忽略错误,请按如下所示添加 #pragma:


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

这将忽略该行上的警告,但仍然允许它贯穿你的项目。

奇怪但真实:如果可以接受( 例如 。 结果是空的,你不介意让runloop循环一次),添加一个延迟,即使这是零:


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

这将删除警告,可能是因为它reassures不可以返回任何对象,也可能是 mismanaged 。

下面是基于上述答案的更新的宏。 这个应该允许你用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]
);

这里代码不涉及编译器标志或者直接运行时调用:


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 不同,这将在任何方法上工作。

在上的Matt答案这个线程解释了为什么:

请考虑以下事项:


id anotherObject1 = [someObject performSelector:@selector(copy)];
id anotherObject2 = [someObject performSelector:@selector(giveMeAnotherNonRetainedObject)];

现在,ARC如何知道第一个返回的是一个带有 1的对象,而第二个返回的对象是 autoreleased?

如果忽略返回值,通常可以安全地禁止显示警告。 如果你真的需要从 performSelector --中获取一个保留的对象而不是"不要这样做",那么我不知道最佳实践是什么。

...