首页 文章

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

提问于
浏览
1213

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

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

这就是我正在做的事情:

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

为什么我会收到这个警告?我理解编译器无法检查选择器是否存在,但为什么会导致泄漏?我怎样才能更改我的代码,以便我不再收到此警告?

19 回答

  • 31

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

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

  • 15

    解决方案

    出于某种原因,编译器会对此发出警告 . 这个警告应该被忽略,这很容易解决 . 这是如何做:

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

    解释

    's going on here is you'要求控制器提供与控制器对应的方法的C函数指针 . 所有 NSObject 都响应 methodForSelector: ,但您也可以在Objective-C运行时使用 class_getMethodImplementation (如果您只有一个协议引用,如 id<SomeProto> ,则非常有用) . 这些函数指针被称为 IMP s,并且是简单的 typedef ed函数指针( id (*IMP)(id, SEL, ...) )1 . 这可能接近方法的实际方法签名,但并不总是完全匹配 .

    获得 IMP 之后,需要将其转换为包含ARC所需的所有详细信息的函数指针(包括每个Objective-C方法调用的两个隐式隐藏参数 self_cmd ) . 这是在第三行处理的(右侧的 (void *) 只是告诉编译器你知道你_582348匹配的是什么) .

    最后,调用函数pointer2 .

    复杂的例子

    当选择器接受参数或返回值时,您将不得不稍微改变一下:

    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

    • 忽略非对象类型( voidint 等)

    • 保留对象值,然后在不再使用时释放(标准假设)

    • 不再使用时释放新对象值( init / copy 系列中的方法或 ns_returns_retained 属性)

    • 不执行任何操作并假设返回的对象值在本地范围内有效(直到内部大多数发布池耗尽,归因于 ns_returns_autoreleased

    methodForSelector: 的调用假定它__82362_重新调用的方法的返回值返回一个新对象) .

    对于您尝试调用返回 void 或其他非对象的选择器,您可以启用编译器功能来忽略警告,但这可能很危险 . 我've seen Clang go through a few iterations of how it handles return values that aren' t分配给局部变量 . 有's no reason that with ARC enabled that it can' t保留并释放从 methodForSelector: 返回的对象值,即使你没有't want to use it. From the compiler'的视角,它毕竟是一个对象 . 这意味着如果您正在调用的方法 someMethod 正在返回非对象(包括 void ),则最终可能会保留/释放垃圾指针值并崩溃 .

    附加参数

    一个考虑因素是,这与 performSelector:withObject: 会发生相同的警告,并且您可能会遇到类似的问题而未声明该方法如何使用参数 . ARC允许声明consumed parameters,如果方法使用参数,则最好只使用上面的 IMP 和函数指针方法 . 由于消耗的参数很少成为问题,因此不太可能出现 .

    静态选择器

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

    [_controller performSelector:@selector(someMethod)];
    

    这是因为编译器实际上能够在编译期间记录有关选择器和对象的所有信息 . 它不需要对任何事情做任何假设 . (我在一年前通过查看来源检查了这一点,但现在没有参考 . )

    压制

    在试图考虑抑制此警告是必要的以及良好的代码设计的情况时,我发现空白 . 有人请分享,如果他们有经验,需要沉默这个警告(并且以上不能正确处理事情) .

    更多

    也可以 Build 一个_582375来处理这个问题,但是这样做需要更多的打字并且速度也更慢,所以没有理由这样做 .

    历史

    performSelector: 系列方法首次添加到Objective-C时,ARC不存在 . 在创建ARC时,Apple决定应为这些方法生成警告,以指导开发人员使用其他方法明确定义在通过命名选择器发送任意消息时应如何处理内存 . 在Objective-C中,开发人员可以通过在原始函数指针上使用C样式转换来实现此目的 .

    随着将Swift,Apple has documented performSelector: 方法系列引入"inherently unsafe"并且它们不适用于Swift .

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

    • 早期版本的Objective-C允许 performSelector: (手动内存管理)

    • 使用ARC的Objective-C警告使用 performSelector:

    • Swift无法访问 performSelector: 并将这些方法记录为"inherently unsafe"

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


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

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

    3实际上,如果将对象声明为 id 并且你只是得到警告它不知道可以选择哪两个方法签名,那么它可能会得到错误的信息 .

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

  • 14

    在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宏系统来隐藏编译指示,则可以定义一个宏以便更容易地抑制警告:

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

    我对此的猜测是这样的:由于编译器不知道选择器,ARC无法强制执行适当的内存管理 .

    实际上,有时候内存管理通过特定约定与方法的名称相关联 . 具体来说,我正在考虑方便构造函数与make方法;前者按惯例返回自动释放的对象;后者是保留的对象 . 约定基于选择器的名称,因此如果编译器不知道选择器,则它无法强制执行适当的内存管理规则 .

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

  • 4

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

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

  • 20

    作为解决方法,直到编译器允许覆盖警告,您可以使用运行时

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

    代替

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

    你必须这样做

    #import <objc/message.h>
    
  • 119

    要仅使用执行选择器在文件中忽略错误,请按如下方式添加#pragma:

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

    这会忽略此行上的警告,但在整个项目的其余部分仍然允许它 .

  • 207

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

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

    这会删除警告,大概是因为它让编译器放心不能返回任何对象并且以某种方式管理不当 .

  • 110

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

    好吧,这里有很多答案,但由于这有点不同,结合几个答案,我以为我会把它放进去 . 我正在使用一个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
    
  • 88

    为了后人的缘故,我决定把帽子扔进戒指:)

    最近我看到越来越多的重组远离 target / __ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ [_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _目标:

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

    这些似乎是一个干净的,ARC安全的,几乎完全相同的替代 performSelector ,而不必太多关于 objc_msgSend() .

    虽然,我不知道iOS上是否有可用的模拟器 .

  • 68

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

    请考虑以下内容:id anotherObject1 = [someObject performSelector:@selector(copy)];
    id anotherObject2 = [someObject performSelector:@selector(giveMeAnotherNonRetainedObject)];
    现在,ARC如何知道第一个返回一个保留计数为1的对象,但第二个返回一个自动释放的对象?

    如果你忽略了,通常可以安全地抑制警告返回值 . 如果你真的需要从performSelector获取一个保留对象,我不确定最佳实践是什么 - 除了“不要那样做” .

  • 1177

    @ 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返回保留的对象 .

    如果您将运行内存泄漏工具,您可以看到以下图片:
    enter image description here
    ...并且在任何其他情况下没有内存泄漏:
    enter image description here

  • 6

    让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];
                    )
    
  • 1

    不要压制警告!

    修补编译器的方法不亚于 12 .
    虽然你在第一次实施时很聪明,但地球上很少有工程师可以跟随你的脚步,这段代码最终会破裂 .

    Safe Routes:

    所有这些解决方案都可以使用,与您最初的意图有一定程度的差异 . 如果您愿意,假设 param 可以是 nil

    Safe route, same conceptual behavior:

    // 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]];
    

    Safe route, slightly different behavior:

    (见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]];
    

    Dangerous Routes

    需要某种编译器静默,这必然会破坏 . 注意,目前, didSwift 中断 .

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

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

  • 34

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

    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];
        }
    
  • 16

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

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

    你也可以在这里使用协议 . 所以,创建一个这样的协议:

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

    在你需要调用选择器的类中,你有一个@property .

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

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

    [self.source doSomethingWithObject:object];
    

相关问题