【问题标题】:performSelector may cause a leak because its selector is unknownperformSelector 可能会导致泄漏,因为它的选择器是未知的
【发布时间】:2011-10-24 10:35:41
【问题描述】:

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

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

这就是我正在做的事情:

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

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

【问题讨论】:

  • 变量的名称是动态的,它取决于很多其他的东西。我称某些东西不存在是有风险的,但这不是问题。
  • @matt 为什么在对象上动态调用方法是不好的做法? NSSelectorFromString() 的全部目的不就是支持这种做法吗?
  • 您应该/可以在通过 performSelector 设置之前测试 [_controller respondsToSelector:mySelector]:
  • @mattacular 希望我能投反对票:“那……是不好的做法。”
  • 如果您知道字符串是文字,只需使用 @selector() 以便编译器知道选择器名称是什么。如果您的实际代码使用在运行时构造或提供的字符串调用 NSSelectorFromString(),那么您必须使用 NSSelectorFromString()。

标签: ios objective-c memory-leaks automatic-ref-counting


【解决方案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:,但您也可以在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,运行时需要知道如何处理您调用的方法的结果。结果可能是任何东西:voidintcharNSString *id 等。ARC 通常从您正在使用的对象类型的标题中获取此信息。3

ARC 只考虑返回值的 4 件事:4

  1. 忽略非对象类型(voidint 等)
  2. 保留对象值,当不再使用时释放(标准假设)
  3. 在不再使用时释放新的对象值(init/copy 系列中的方法或ns_returns_retained 属性)
  4. 什么都不做,假设返回的对象值在本地范围内有效(直到最里面的释放池被耗尽,归因于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。

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

  1. Objective-C 的早期版本允许 performSelector:(手动内存管理)
  2. 带有 ARC 的 Objective-C 警告使用 performSelector:
  3. Swift 无权访问 performSelector:,并将这些方法记录为“本质上不安全”

然而,基于命名选择器发送消息的想法并不是“天生不安全”的特性。这个想法已经在 Objective-C 以及许多其他编程语言中成功使用了很长时间。


1 所有的 Objective-C 方法都有两个隐藏的参数,self_cmd,它们会在你调用方法时隐式添加。

2 在 C 中调用 NULL 函数是不安全的。用于检查控制器是否存在的守卫确保我们有一个对象。因此,我们知道我们将从methodForSelector: 获得IMP(尽管它可能是_objc_msgForward,进入消息转发系统)。基本上,有了守卫,我们就知道我们有一个函数可以调用。

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

4有关详细信息,请参阅retained return valuesunretained return values 上的 ARC 参考。

【讨论】:

  • @wbyoung 如果您的代码解决了保留问题,我想知道为什么 performSelector: 方法没有以这种方式实现。它们有严格的方法签名(返回id,取一两个ids),所以不需要处理原始类型。
  • @Andy 根据方法原型的定义处理参数(不会保留/释放)。关注点主要基于返回类型。
  • 使用最新的 Xcode 时,“复杂示例”会给出错误 Cannot initialize a variable of type 'CGRect (*)(__strong id, SEL, CGRect, UIView *__strong)' with an rvalue of type 'void *'。 (5.1.1) 尽管如此,我还是学到了很多!
  • void (*func)(id, SEL) = (void *)imp; 无法编译,我已将其替换为void (*func)(id, SEL) = (void (*)(id, SEL))imp;
  • void (*func)(id, SEL) = (void *)imp; 更改为<…> = (void (*))imp;<…> = (void (*) (id, SEL))imp;
【解决方案2】:

在 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 宏系统来隐藏 pragma,您可以定义一个宏以便更轻松地抑制警告:

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

【讨论】:

  • 当优化设置为 None 以外的任何值时,此方法可能会导致内存泄漏。
  • @Eric 不,它不能,除非你调用有趣的方法,比如“initSomething”或“newSomething”或“somethingCopy”。
  • @Julian 这确实有效,但会关闭整个文件的警告——你可能不需要或不想要那个。用poppush-pragmas 包裹它会更干净、更安全。
  • 这一切只是让编译器静音。这并不能解决问题。如果选择器不存在,那你就完蛋了。
  • 这只能在被if ([_target respondsToSelector:_selector]) { 或类似逻辑包裹时使用。
【解决方案3】:

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

事实上,有时内存管理通过特定约定与方法名称相关联。具体来说,我正在考虑 convenience constructorsmake 方法;前者按惯例返回一个自动释放的对象;后者是保留对象。该约定基于选择器的名称,因此如果编译器不知道选择器,则无法执行正确的内存管理规则。

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

【讨论】:

  • 感谢您的回答,我会对此进行更多研究,看看发生了什么。关于如何绕过警告并使其消失的任何想法?我不想让警告永远留在我的代码中,因为这是一个安全的调用。
  • 所以我在他们的论坛上得到了 Apple 某人的确认,确实是这样。他们将添加一个被遗忘的覆盖,以允许人们在未来的版本中禁用此警告。谢谢。
  • 这个答案提出了一些问题,比如如果 ARC 试图根据约定和方法名称来决定何时发布某些东西,那么它是如何“引用计数”的?如果 ARC 假设代码遵循某种约定,而不是不管遵循什么约定,实际跟踪引用,那么您描述的行为听起来只比完全任意的好一点。
  • ARC 在编译时自动执行添加保留和发布的过程。它不是垃圾收集(这就是为什么它如此快速和低开销的原因)。这根本不是任意的。默认规则基于几十年来一直应用的成熟的 ObjC 约定。这避免了在解释其内存管理的每个方法中显式添加__attribute 的需要。但这也使得编译器无法正确处理这种模式(这种模式过去很常见,但近年来已被更强大的模式取代)。
  • 所以我们不能再拥有SEL 类型的ivar 并根据情况分配不同的选择器?走的路,动态语言...
【解决方案4】:

在您的项目构建设置中,在其他警告标志 (WARNING_CFLAGS)下,添加
-Wno-arc-performSelector-leaks

现在只需确保您调用的选择器不会导致您的对象被保留或复制。

【讨论】:

  • 请注意,您可以为特定文件而不是整个项目添加相同的标志。如果您在 Build Phases->Compile Sources 下查看,您可以设置每个文件的编译器标志(就像您想要从 ARC 中排除文件一样)。在我的项目中,只有一个文件应该以这种方式使用选择器,所以我只是将其排除在外,留下了其他文件。
【解决方案5】:

在编译器允许覆盖警告之前,作为一种解决方法,您可以使用运行时。

你需要标题:

#import <objc/message.h>

然后试试下面:

// For strict compilers.
((id(*)(id,SEL))objc_msgSend)(_controller, sel_getUid("someMethod"));

// Old answer's code:
objc_msgSend(_controller, NSSelectorFromString(@"someMethod"));

代替:

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

【讨论】:

  • ARC 识别 Cocoa 约定,然后根据这些约定添加保留和发布。因为 C 不遵循这些约定,ARC 强制您使用手动内存管理技术。如果你创建一个 CF 对象,你必须 CFRelease() 它。如果你 dispatch_queue_create(),你必须 dispatch_release()。底线,如果你想避免 ARC 警告,你可以通过使用 C 对象和手动内存管理来避免它们。此外,您可以使用该文件上的 -fno-objc-arc 编译器标志在每个文件的基础上禁用 ARC。
  • 没有铸造,你不能。 Varargs 与显式类型的参数列表不同。它通常会巧合,但我不认为“巧合”是正确的。
  • 不要那样做,[_controller performSelector:NSSelectorFromString(@"someMethod")];objc_msgSend(_controller, NSSelectorFromString(@"someMethod")); 不是等价的!看看Method Signature MismatchesA big weakness in Objective-C's weak typing 他们正在深入解释这个问题。
  • @0xced 在这种情况下,没关系。 objc_msgSend 不会为任何可以在 performSelector: 或其变体中正常工作的选择器创建方法签名不匹配,因为它们只将对象作为参数。只要你的所有参数都是指针(包括对象)、双精度数和 NSInteger/long,并且你的返回类型是 void、指针或 long,那么 objc_msgSend 就可以正常工作。
  • ObjC 没有像 c++ 那样的函数重载。因此,即使 mikeash 的网站表达了真正的担忧,当您尝试重载(不意味着覆盖 - 以防有人混淆这些词)由于 ObjC 而无法重载的方法时,您应该得到编译器警告。
【解决方案6】:

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

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

这将忽略这一行的警告,但在整个项目的其余部分仍然允许它。

【讨论】:

  • 我猜你也可以在使用#pragma clang diagnostic warning "-Warc-performSelector-leaks" 的方法之后立即重新打开警告。我知道如果我关闭警告,我喜欢尽快将其重新打开,所以我不会不小心让另一个意料之外的警告溜走。这不太可能是个问题,但这只是我关闭警告时的习惯。
  • 您还可以在进行任何更改之前使用#pragma clang diagnostic warning push 恢复之前的编译器配置状态,并使用#pragma clang diagnostic warning pop 恢复之前的状态。如果您要关闭负载并且不想在代码中包含大量重新启用的编译指示行,这很有用。
  • 只会忽略下面一行?
【解决方案7】:

奇怪但真实:如果可以接受(即结果为 void 并且您不介意让 runloop 循环一次),请添加延迟,即使这是零:

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

这消除了警告,大概是因为它向编译器保证不会返回任何对象并且不会以某种方式管理不善。

【讨论】:

  • 你知道这是否真的解决了相关的内存管理问题,还是有同样的问题,但 Xcode 不够聪明,无法用这段代码警告你?
  • 这在语义上是不一样的!使用 performSelector:withObject:AfterDelay: 将在 runloop 的下一次运行中执行选择器。因此,该方法立即返回。
  • @Florian 当然不一样!阅读我的回答:我说 if 可以接受,因为结果是 void 并且 runloop 循环。这是我回答的第一句话
【解决方案8】:

这是基于上面给出的答案的更新宏。这应该允许您使用 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]
);

【讨论】:

  • return 不必在宏内部; return SUPPRESS_PERFORM_SELECTOR_LEAK_WARNING([_target performSelector:_action withObject:self]); 也可以工作并且看起来更健康。
【解决方案9】:

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

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 不同,这适用于任何方法。

【讨论】:

  • 你知道这是否真的解决了相关的内存管理问题,还是有同样的问题,但 Xcode 不够聪明,无法用这段代码警告你?
  • 你可以说它解决了内存管​​理问题;但这是因为它基本上可以让您指定行为。例如,您可以选择让调用保留或不保留参数。据我目前所知,它试图通过相信您知道自己在做什么并且不向其提供不正确的数据来解决可能出现的签名不匹配问题。我不确定是否可以在运行时执行所有检查。正如另一条评论中提到的,mikeash.com/pyblog/… 很好地解释了不匹配可以做什么。
【解决方案10】:

好吧,这里有很多答案,但由于这有点不同,所以结合了一些我认为我会放入的答案。我正在使用一个 NSObject 类别来检查以确保选择器返回 void,并且禁止编译器警告。

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import "Debug.h" // not given; just an assert

@interface NSObject (Extras)

// Enforce the rule that the selector used must return void.
- (void) performVoidReturnSelector:(SEL)aSelector withObject:(id)object;
- (void) performVoidReturnSelector:(SEL)aSelector;

@end

@implementation NSObject (Extras)

// Apparently the reason the regular performSelect gives a compile time warning is that the system doesn't know the return type. I'm going to (a) make sure that the return type is void, and (b) disable this warning
// See http://stackoverflow.com/questions/7017281/performselector-may-cause-a-leak-because-its-selector-is-unknown

- (void) checkSelector:(SEL)aSelector {
    // See http://stackoverflow.com/questions/14602854/objective-c-is-there-a-way-to-check-a-selector-return-value
    Method m = class_getInstanceMethod([self class], aSelector);
    char type[128];
    method_getReturnType(m, type, sizeof(type));

    NSString *message = [[NSString alloc] initWithFormat:@"NSObject+Extras.performVoidReturnSelector: %@.%@ selector (type: %s)", [self class], NSStringFromSelector(aSelector), type];
    NSLog(@"%@", message);

    if (type[0] != 'v') {
        message = [[NSString alloc] initWithFormat:@"%@ was not void", message];
        [Debug assertTrue:FALSE withMessage:message];
    }
}

- (void) performVoidReturnSelector:(SEL)aSelector withObject:(id)object {
    [self checkSelector:aSelector];

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    // Since the selector (aSelector) is returning void, it doesn't make sense to try to obtain the return result of performSelector. In fact, if we do, it crashes the app.
    [self performSelector: aSelector withObject: object];
#pragma clang diagnostic pop    
}

- (void) performVoidReturnSelector:(SEL)aSelector {
    [self checkSelector:aSelector];

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    [self performSelector: aSelector];
#pragma clang diagnostic pop
}

@end

【讨论】:

  • 应该用_C_VOID替换'v'吗? _C_VOID 在 中声明。
【解决方案11】:

为了子孙后代,我决定把我的帽子扔进擂台:)

最近我看到越来越多的重组从 target/selector 范式转向诸如协议、块等之类的东西。但是,performSelector 有一个替代品我已经用过几次了:

[NSApp sendAction: NSSelectorFromString(@"someMethod") to: _controller from: nil];

这些似乎是 performSelector 的干净、ARC 安全且几乎相同的替代品,而无需过多关注 objc_msgSend()

不过,我不知道 iOS 上是否有可用的模拟。

【讨论】:

  • 感谢您提供此内容。它在 iOS 中可用:[[UIApplication sharedApplication] sendAction: to: from: forEvent:]。我看过一次,但是在你的域或服务中间使用一个与 UI 相关的类只是为了进行动态调用有点尴尬。谢谢你包括这个!
  • 呃!它将有更多的开销(因为它需要检查该方法是否可用,如果不可用,则沿着响应者链向上走)并且具有不同的错误行为(沿着响应者链向上走,如果找不到任何东西,则返回 NO它响应方法,而不是简单地崩溃)。当您想要来自-performSelector:...id 时,它也不起作用
  • @tc。除非 to: 为 nil,否则它不会“沿着响应者链向上走”,但事实并非如此。它只是直接到达目标对象而无需事先检查。所以没有“更多的开销”。这不是一个很好的解决方案,但你给出的理由不是理由。 :)
【解决方案12】:

Matt Galloway 在this thread 上的回答解释了原因:

考虑以下几点:

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

现在,ARC 怎么知道第一个返回的对象保留计数为 1,但第二个 返回一个自动释放的对象?

如果您忽略返回值,抑制警告似乎通常是安全的。如果您确实需要从 performSelector 获取保留对象,我不确定最佳做法是什么——除了“不要那样做”。

【讨论】:

    【解决方案13】:

    @c-road 提供了问题描述here 的正确链接。下面你可以看到我的例子,当 performSelector 导致内存泄漏时。

    @interface Dummy : NSObject <NSCopying>
    @end
    
    @implementation Dummy
    
    - (id)copyWithZone:(NSZone *)zone {
      return [[Dummy alloc] init];
    }
    
    - (id)clone {
      return [[Dummy alloc] init];
    }
    
    @end
    
    void CopyDummy(Dummy *dummy) {
      __unused Dummy *dummyClone = [dummy copy];
    }
    
    void CloneDummy(Dummy *dummy) {
      __unused Dummy *dummyClone = [dummy clone];
    }
    
    void CopyDummyWithLeak(Dummy *dummy, SEL copySelector) {
      __unused Dummy *dummyClone = [dummy performSelector:copySelector];
    }
    
    void CloneDummyWithoutLeak(Dummy *dummy, SEL cloneSelector) {
      __unused Dummy *dummyClone = [dummy performSelector:cloneSelector];
    }
    
    int main(int argc, const char * argv[]) {
      @autoreleasepool {
        Dummy *dummy = [[Dummy alloc] init];
        for (;;) { @autoreleasepool {
          //CopyDummy(dummy);
          //CloneDummy(dummy);
          //CloneDummyWithoutLeak(dummy, @selector(clone));
          CopyDummyWithLeak(dummy, @selector(copy));
          [NSThread sleepForTimeInterval:1];
        }} 
      }
      return 0;
    }
    

    在我的示例中导致内存泄漏的唯一方法是 CopyDummyWithLeak。原因是 ARC 不知道,copySelector 返回的是保留对象。

    如果您将运行 Memory Leak Tool,您可以看到以下图片: ...并且在任何其他情况下都没有内存泄漏:

    【讨论】:

      【解决方案14】:

      让 Scott Thompson 的宏更通用:

      // String expander
      #define MY_STRX(X) #X
      #define MY_STR(X) MY_STRX(X)
      
      #define MYSilenceWarning(FLAG, MACRO) \
      _Pragma("clang diagnostic push") \
      _Pragma(MY_STR(clang diagnostic ignored MY_STR(FLAG))) \
      MACRO \
      _Pragma("clang diagnostic pop")
      

      然后像这样使用它:

      MYSilenceWarning(-Warc-performSelector-leaks,
      [_target performSelector:_action withObject:self];
                      )
      

      【讨论】:

      • FWIW,我没有添加宏。有人在我的回复中添加了这一点。就个人而言,我不会使用宏。 pragma 用于解决代码中的特殊情况,并且 pragma 非常明确和直接地说明正在发生的事情。我更喜欢将它们保留在适当的位置,而不是将它们隐藏或抽象到宏后面,但这只是我。 YMMV。
      • @ScottThompson 这很公平。对我来说,在我的代码库中搜索这个宏很容易,而且我通常还会添加一个非静默警告来处理潜在问题。
      【解决方案15】:

      不要隐藏警告!

      修补编译器的替代解决方案不少于 12 个。
      虽然你在第一次实施时很聪明,但地球上很少有工程师能追随你的脚步,而这段代码最终会崩溃。

      安全路线:

      所有这些解决方案都会起作用,但与您的初衷有所不同。如果您愿意,假设param 可以是nil

      安全路线,相同的概念行为:

      // GREAT
      [_controller performSelectorOnMainThread:selector withObject:anArgument waitUntilDone:YES];
      [_controller performSelectorOnMainThread:selector withObject:anArgument waitUntilDone:YES modes:@[(__bridge NSString *)kCFRunLoopDefaultMode]];
      
      [_controller performSelector:selector onThread:[NSThread mainThread] withObject:anArgument waitUntilDone:YES];
      [_controller performSelector:selector onThread:[NSThread mainThread] withObject:anArgument waitUntilDone:YES modes:@[(__bridge NSString *)kCFRunLoopDefaultMode]];
      

      安全路线,行为略有不同:

      (请参阅 this 回复)
      使用任何线程代替[NSThread mainThread]

      // GOOD
      [_controller performSelector:selector withObject:anArgument afterDelay:0];
      [_controller performSelector:selector withObject:anArgument afterDelay:0 inModes:@[(__bridge NSString *)kCFRunLoopDefaultMode]];
      
      [_controller performSelectorOnMainThread:selector withObject:anArgument waitUntilDone:NO];
      [_controller performSelectorOnMainThread:selector withObject:anArgument waitUntilDone:NO];
      [_controller performSelectorOnMainThread:selector withObject:anArgument waitUntilDone:NO modes:@[(__bridge NSString *)kCFRunLoopDefaultMode]];
      
      [_controller performSelectorInBackground:selector withObject:anArgument];
      
      [_controller performSelector:selector onThread:[NSThread mainThread] withObject:anArgument waitUntilDone:NO];
      [_controller performSelector:selector onThread:[NSThread mainThread] withObject:anArgument waitUntilDone:NO modes:@[(__bridge NSString *)kCFRunLoopDefaultMode]];
      

      危险路线

      需要某种编译器静默,这势必会中断。请注意,目前它确实Swift 中中断。

      // AT YOUR OWN RISK
      [_controller performSelector:selector];
      [_controller performSelector:selector withObject:anArgument];
      [_controller performSelector:selector withObject:anArgument withObject:nil];
      

      【讨论】:

      • 措辞非常错误。安全路线根本不比危险更安全。可以说它更危险,因为它隐含地隐藏了警告。
      • 我会修正措辞,避免侮辱,但我信守诺言。我唯一可以接受的静音警告是如果我不拥有代码。没有工程师可以在不了解所有后果的情况下安全地维护沉默的代码,这意味着阅读这个论点,这种做法是有风险的;特别是如果您考虑 12 种简单的英语、可靠的替代方案。
      • 没有。你没明白我的意思。使用performSelectorOnMainThread不是消除警告的好方法,它有副作用。 (它不能解决内存泄漏)额外的#clang diagnostic ignored 以非常清晰的方式显式抑制警告。
      • 确实,在非 - (void) 方法上执行选择器是真正的问题。
      • 你如何通过这个调用具有多个参数的选择器并同时保持安全? @SwiftArchitect
      【解决方案16】:

      因为您使用的是 ARC,所以您必须使用 iOS 4.0 或更高版本。这意味着您可以使用块。如果不是记住选择器来执行,而是执行一个块,ARC 将能够更好地跟踪实际发生的事情,并且您不必冒意外引入内存泄漏的风险。

      【讨论】:

      • 实际上,块很容易意外创建 ARC 无法解决的保留循环。我仍然希望当您通过 ivar 隐式使用 self 时出现编译器警告(例如 ivar 而不是 self-&gt;ivar)。
      • 你的意思是像 -Wimplicit-retain-self ?
      【解决方案17】:

      而不是使用块方法,这给了我一些问题:

          IMP imp = [_controller methodForSelector:selector];
          void (*func)(id, SEL) = (void *)imp;
      

      我将使用 NSInvocation,如下所示:

          -(void) sendSelectorToDelegate:(SEL) selector withSender:(UIButton *)button 
      
          if ([delegate respondsToSelector:selector])
          {
          NSMethodSignature * methodSignature = [[delegate class]
                                          instanceMethodSignatureForSelector:selector];
          NSInvocation * delegateInvocation = [NSInvocation
                                         invocationWithMethodSignature:methodSignature];
      
      
          [delegateInvocation setSelector:selector];
          [delegateInvocation setTarget:delegate];
      
          // remember the first two parameter are cmd and self
          [delegateInvocation setArgument:&button atIndex:2];
          [delegateInvocation invoke];
          }
      

      【讨论】:

        【解决方案18】:

        如果您不需要传递任何参数,一个简单的解决方法是使用valueForKeyPath。这甚至可以在 Class 对象上实现。

        NSString *colorName = @"brightPinkColor";
        id uicolor = [UIColor class];
        if ([uicolor respondsToSelector:NSSelectorFromString(colorName)]){
            UIColor *brightPink = [uicolor valueForKeyPath:colorName];
            ...
        }
        

        【讨论】:

          【解决方案19】:

          您也可以在这里使用协议。所以,像这样创建一个协议:

          @protocol MyProtocol
          -(void)doSomethingWithObject:(id)object;
          @end
          

          在需要调用选择器的类中,您将拥有一个@property。

          @interface MyObject
              @property (strong) id<MyProtocol> source;
          @end
          

          当您需要在 MyObject 的实例中调用 @selector(doSomethingWithObject:) 时,请执行以下操作:

          [self.source doSomethingWithObject:object];
          

          【讨论】:

          • Hey Wu,谢谢,但使用 NSSelectorFromString 的关键在于您不知道在运行时要调用哪个选择器。
          猜你喜欢
          • 2014-02-21
          • 2014-01-07
          • 2013-08-04
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-06-16
          • 1970-01-01
          相关资源
          最近更新 更多