Target-Action 设计模式在 ARC 下是不是变成了不好的做法?

Posted

技术标签:

【中文标题】Target-Action 设计模式在 ARC 下是不是变成了不好的做法?【英文标题】:Did the Target-Action design pattern became bad practice under ARC?Target-Action 设计模式在 ARC 下是否变成了不好的做法? 【发布时间】:2012-02-09 22:40:59 【问题描述】:

多年来,我一直在遵循一种名为 Target-Action 的伟大模式,它是这样的:

对象在调用的时候调用指定目标对象上的指定选择器。这在您需要对任意方法进行简单回调的许多不同情况下非常有用。

这是一个例子:

- (void)itemLoaded 
    [specifiedReceiver performSelector:specifiedSelector];

现在事实证明,在 ARC 下,突然做这样的事情变得很危险。

Xcode 抛出如下警告:

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

当然选择器是未知的,因为作为 T​​arget-Action 设计模式的一部分,您可以指定您想要的任何选择器,以便在发生有趣的事情时获得调用。

这个警告最让我烦恼的是它说可能存在潜在的内存泄漏。据我了解,ARC 并没有改变内存管理规则,而是简单地在正确的位置自动插入保留/释放/自动释放消息。

这里要注意的另一件事:-performSelector: 确实有一个id 返回值。 ARC 分析方法签名以通过应用命名约定来确定该方法是否返回 +1 保留计数对象。在这种情况下,ARC 不知道选择器是 -newFooBar 工厂还是简单地调用不可疑的工作方法(无论如何 Target-Action 几乎总是如此)。实际上 ARC 应该已经认识到我不期望返回值,因此忘记任何潜在的 +1 保留计数返回值。从这个角度来看,我可以看到 ARC 的来源,但对于这在实践中的真正含义仍然存在太多不确定性。

现在这是否意味着在 ARC 下可能会出现问题,而如果没有 ARC,就永远不会发生这种情况?我不明白这会如何产生内存泄漏。有人可以举例说明这样做很危险的情况,以及在这种情况下究竟是如何造成泄漏的?

我真的用谷歌搜索了互联网,但没有找到任何解释原因的网站。

【问题讨论】:

【参考方案1】:

如上所述,您会收到该警告,因为编译器不知道将 performSelector 的保留/释放放在哪里(或是否):返回值。

但是请注意,如果您使用[someObject performSelector:@selector(selectorName)],它不会产生警告(至少在带有 llvm 4.1 的 Xcode 4.5 中),因为确切的选择器很容易确定(您明确设置它),这就是编译器能够放置的原因保留/释放在正确的位置。

这就是为什么只有在使用 SEL 指针传递选择器时才会收到警告,因为在这种情况下编译器无法确定在所有情况下要做什么。所以使用以下

SEL s = nil;
if(condition1) SEL = @selector(sel1)
else SEL = @selector(sel2)

[self performSelector:s];

会产生警告。但将其重构为:

if(condition1) [self performSelector:@selector(sel1)]
else [self performSelector:@selector(sel2)]

不会产生任何警告

【讨论】:

【参考方案2】:

performSelector 的问题在于 ARC 不知道选择器将执行什么操作。考虑以下几点:

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

现在,ARC 怎么知道第一个返回一个保留计数为 1 的对象,而第二个返回一个自动释放的对象? (我只是在这里定义了一个名为giveMeAnotherNonRetainedObject 的方法,它返回自动释放的内容)。如果它没有添加任何版本,那么anotherObject1 会在这里泄漏。

显然,在我的示例中,要执行的选择器实际上是已知的,但假设它们是在运行时选择的。 ARC 确实无法在此处输入正确数量的retains 或releases,因为它根本不知道选择器将要做什么。你说得对,ARC 没有违反任何规则,它只是为你添加了正确的内存管理调用,但这正是它不能在这里做的事情。

您是对的,您忽略返回值这一事实意味着它会没问题,但总的来说 ARC 只是挑剔和警告。但我想这就是为什么它是警告而不是错误的原因。

编辑:

如果你真的确定你的代码没问题,你可以像这样隐藏警告:

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[specifiedReceiver performSelector:specifiedSelector];
#pragma clang diagnostic pop

【讨论】:

你完全正确。但在我的情况下,我完全忽略了返回值,这在逻辑上意味着这个位置不会有问题。我什至尝试过强制转换为 void、桥接、__unsafe_unretained 等 - 摆脱它的唯一方法是移动到块(这会导致大量其他问题)或禁用构建阶段设置中的警告。 查看我的编辑以关闭一小段代码的警告。 两个大脑,同一个想法。伟大的! :-)(同时输入我的答案。包含有关禁用警告的更多信息) @JoshCaswell - 公平点。我已将其更改为giveMeAnotherNonRetainedObject 现象级的。刚刚遇到了我的这个老问题,并意识到您在一年后又回答了一个问题。谢谢。【参考方案3】:

警告应该是这样的:

PerformSelector 可能会导致泄漏,因为它的选择器是未知的。 ARC 不知道返回的 id 是否有 +1 保留计数,因此无法正确管理返回对象的内存。

不幸的是,这只是第一句话。

现在解决办法:

如果您从 -performSelector 方法收到返回值,则您无法对代码中的警告执行任何操作,除非忽略它。

NSArray *linkedNodes = [startNode performSelector:nodesArrayAccessor];

您最好的选择是:

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
NSArray *linkedNodes = [startNode performSelector:nodesArrayAccessor];
#pragma clang diagnostic pop

我最初的问题也是如此,我完全忽略了返回值。 ARC 应该足够聪明,可以看到我不关心返回的 id,因此匿名选择器几乎可以保证不是工厂、便利构造函数或任何其他东西。不幸的是 ARC 不是,所以同样的规则也适用。忽略警告。

也可以通过在项目构建设置中的“其他警告标志”下设置 -Wno-arc-performSelector-leaks 编译器标志来为整个项目完成。

或者,您可以在所需文件旁边右侧的“目标”>“构建阶段”>“编译源”下添加该标志时,以每个文件为基础隐藏警告。

恕我直言,所有三个解决方案都非常混乱,所以我希望有人提出更好的解决方案。

【讨论】:

【参考方案4】:

ARC 发出警告是因为它不能保证选择器不会创建它不知道的对象。从理论上讲,您可以从该方法中获得 ARC 无法处理的内容:

id objectA = [someObject performSelector:@selector(createObjectA)];

也许有一天它可以,但现在它不能。 (请注意,如果它确实知道对象(它不是 id),它不会抛出此警告)。

如果您想简单地执行一个方法而不从它接收一个对象,我建议使用 objc_msgSend。但你必须包括在你的班级:

#include <objc/message.h>
objc_msgSend(someObject, action);

【讨论】:

更好的是,使用类型化块作为完成处理程序,而不是指定任意选择器。 有人告诉我直接调用 objc_msgSend 有很多缺点,在某些情况下可能很危险。 是的,我已将所有自定义类从“委托”模型移至使用块。但是你真的必须小心内存管理和块。使用委托模型,控件可以通过使用对委托的弱引用来中断保留周期。但是在引用 iVar 时,块会自动保留自身。因此,任何分配块的类都必须确保它不会强烈引用自身,如果它还保持对控件(存储块)的引用。我相信这就是为什么 Apple 不会使用完成块来代替委托。 @MikhaloIvanokov objc_msgSend 很危险,因为你也可以从它那里收到一个对象,从而弄乱 ARC(编译器不会抱怨)。只要确保你正在执行一些返回 void 的操作,我认为你会没事的。 要使用 objc_msgSend(这是正确的解决方案),将调用类型转换为正确的函数指针(返回 void,并采用正确的参数)。 IE。 ((void (*)(id,SEL,typeof(self)))objc_msgSend)( self.target, self.action, self );

以上是关于Target-Action 设计模式在 ARC 下是不是变成了不好的做法?的主要内容,如果未能解决你的问题,请参考以下文章

0c-42-ARC模式下如何兼容非ARC的类

@synthesize of 'weak' property 仅允许在 ARC 或 GC 模式下使用 urbanship 首次编译

在协议扩展中添加 Target-Action 失败

Objective-C关于非ARC模式下的对象引用计数

IOS开发之简单音频播放器

Delegate,Block,Notification, KVC,KVO,Target-Action