解决方案
编译器对此发出警告是有原因的。这个警告很少被忽略,而且很容易解决。方法如下:
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:,但您也可以在Objective-C 运行时中使用class_getMethodImplementation(如果您只有协议引用,则很有用,例如id<SomeProto>)。这些函数指针称为IMPs,是简单的typedefed 函数指针(id (*IMP)(id, SEL, ...))1。这可能接近方法的实际方法签名,但并不总是完全匹配。
一旦你有了IMP,你需要把它转换成一个函数指针,其中包含ARC需要的所有细节(包括每个Objective-C方法调用的两个隐式隐藏参数self和_cmd )。这是在第三行处理的(右侧的(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
- 忽略非对象类型(
void、int 等)
- 保留对象值,当不再使用时释放(标准假设)
- 在不再使用时释放新的对象值(
init/copy 系列中的方法或ns_returns_retained 属性)
- 什么都不做,假设返回的对象值在本地范围内有效(直到最里面的释放池被耗尽,归因于
ns_returns_autoreleased)
对methodForSelector: 的调用假定它调用的方法的返回值是一个对象,但不保留/释放它。因此,如果您的对象应该像上面的 #3 那样被释放(也就是说,您调用的方法返回一个新对象),您最终可能会造成泄漏。
对于您尝试调用返回 void 或其他非对象的选择器,您可以启用编译器功能以忽略警告,但这可能很危险。我已经看到 Clang 对它如何处理未分配给局部变量的返回值进行了几次迭代。没有理由启用 ARC,即使您不想使用它,它也无法保留和释放从 methodForSelector: 返回的对象值。从编译器的角度来看,它毕竟是一个对象。这意味着如果您正在调用的方法 someMethod 返回一个非对象(包括 void),您最终可能会得到一个垃圾指针值被保留/释放并崩溃。
其他参数
一个考虑因素是performSelector:withObject: 会出现同样的警告,如果不声明该方法如何使用参数,您可能会遇到类似的问题。 ARC 允许声明consumed parameters,如果该方法使用该参数,您最终可能会向僵尸发送消息并崩溃。有一些方法可以通过桥接转换来解决这个问题,但实际上最好简单地使用上面的IMP 和函数指针方法。由于消耗的参数很少成为问题,因此不太可能出现。
静态选择器
有趣的是,编译器不会抱怨静态声明的选择器:
[_controller performSelector:@selector(someMethod)];
这样做的原因是因为编译器实际上能够在编译过程中记录有关选择器和对象的所有信息。它不需要对任何事情做任何假设。 (我在一年前通过查看源检查了这一点,但现在没有参考。)
抑制
在尝试考虑抑制此警告是必要的和良好的代码设计的情况时,我一无所获。如果有人有过需要消除此警告的经验,请分享(并且上述内容无法正确处理)。
更多
也可以建立一个NSMethodInvocation 来处理这个问题,但是这样做需要更多的输入并且速度也很慢,所以没有理由这样做。
历史
当 performSelector: 系列方法首次添加到 Objective-C 时,ARC 并不存在。在创建 ARC 时,Apple 决定应该为这些方法生成警告,以指导开发人员使用其他方式明确定义在通过命名选择器发送任意消息时应如何处理内存。在 Objective-C 中,开发人员可以通过对原始函数指针使用 C 风格强制转换来做到这一点。
随着 Swift 的引入,Apple has documented performSelector: 系列方法“本质上是不安全的”,它们不适用于 Swift。
随着时间的推移,我们看到了这种进展:
- Objective-C 的早期版本允许
performSelector:(手动内存管理)
- 带有 ARC 的 Objective-C 警告使用
performSelector:
- Swift 无权访问
performSelector:,并将这些方法记录为“本质上不安全”
然而,基于命名选择器发送消息的想法并不是“天生不安全”的特性。这个想法已经在 Objective-C 以及许多其他编程语言中成功使用了很长时间。
1 所有的 Objective-C 方法都有两个隐藏的参数,self 和 _cmd,它们会在你调用方法时隐式添加。
2 在 C 中调用 NULL 函数是不安全的。用于检查控制器是否存在的守卫确保我们有一个对象。因此,我们知道我们将从methodForSelector: 获得IMP(尽管它可能是_objc_msgForward,进入消息转发系统)。基本上,有了守卫,我们就知道我们有一个函数可以调用。
3 实际上,如果将您的对象声明为id 并且您没有导入所有标头,则它可能会获得错误的信息。您最终可能会导致编译器认为没问题的代码崩溃。这是非常罕见的,但可能会发生。通常你只会得到一个警告,它不知道从两个方法签名中选择哪一个。
4有关详细信息,请参阅retained return values 和 unretained return values 上的 ARC 参考。