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

performSelector may cause a leak because its selector is unknown

ARC编译器发出以下警告:

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

我要做的是:

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

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


解决方案

编译器对此发出警告是有原因的。很少有人会忽视这个警告,而且很容易解决。以下是如何:好的。

1
2
3
4
5
if (!_controller) { return; }
SEL selector = NSSelectorFromString(@"someMethod");
IMP imp = [_controller methodForSelector:selector];
void (*func)(id, SEL) = (void *)imp;
func(_controller, selector);

或者更简洁地说(虽然没有防护装置很难阅读和理解):好的。

1
2
SEL selector = NSSelectorFromString(@"someMethod");
((void (*)(id, SEL))[_controller methodForSelector:selector])(_controller, selector);

解释

这里要做的是,您向控制器请求与控制器对应的方法的C函数指针。所有NSObject都对methodForSelector:作出响应,但也可以在objective-c运行时使用class_getMethodImplementation(如果只有协议引用,如id时才有用)。这些函数指针称为IMPs,是简单的typedefed函数指针(id (*IMP)(id, SEL, ...)1)。这可能接近方法的实际方法签名,但并不总是完全匹配。好的。

一旦拥有了IMP,就需要将其强制转换为一个函数指针,该指针包含ARC需要的所有细节(包括每个objective-c方法调用的两个隐式隐藏参数self_cmd)。这在第三行中处理(右侧的(void *)简单地告诉编译器您知道自己在做什么,并且不会生成警告,因为指针类型不匹配)。好的。

最后,调用函数指针2。好的。复杂实例

当选择器接受参数或返回一个值时,您必须稍微改变一下:好的。

1
2
3
4
5
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:的调用假定它调用的方法的返回值是一个对象,但不保留/释放它。因此,如果您的对象应该像上面的3中那样被释放(即,您调用的方法返回一个新对象),那么您最终可能会创建一个泄漏。好的。

    对于试图调用返回void或其他非对象的选择器,可以启用编译器功能忽略警告,但这可能很危险。我已经看到clang通过一些迭代来处理未分配给局部变量的返回值。启用ARC后,即使您不想使用它,也没有理由不能保留和释放从methodForSelector:返回的对象值。从编译器的角度来看,它毕竟是一个对象。这意味着,如果您调用的方法someMethod返回的是非对象(包括void),则最终可能会导致垃圾指针值被保留/释放并崩溃。好的。其他参数

    一个考虑因素是,这与performSelector:withObject:会出现相同的警告,您可能会遇到类似的问题,而不声明该方法如何使用参数。ARC允许声明已使用的参数,如果该方法使用该参数,您可能最终会向僵尸发送消息并崩溃。有很多方法可以解决这个问题,但实际上最好简单地使用上面的IMP和函数指针方法。由于消耗的参数很少是一个问题,所以不太可能出现这种情况。好的。静态选择器

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

    1
    [_controller performSelector:@selector(someMethod)];

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

    在试图思考一种情况,在这种情况下抑制这个警告是必要的和良好的代码设计,我来这里空白。如果有人有必要停止此警告的经验,请与他们分享(而上述情况处理不当)。好的。更多

    也可以建立一个NSMethodInvocation来处理这个问题,但是这样做需要更多的输入,而且速度也较慢,所以没有什么理由这样做。好的。历史

    performSelector:方法家族首次加入到OBJECT-C中时,ARC不存在。在创建ARC时,苹果决定为这些方法生成一个警告,作为指导开发人员使用其他方法来明确定义当通过命名选择器发送任意消息时如何处理内存的一种方法。在Objective-C中,开发人员可以通过在原始函数指针上使用C样式的强制转换来实现这一点。好的。

    随着Swift的引入,苹果已经将performSelector:系列方法记录为"固有的不安全",Swift无法使用这些方法。好的。

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

  • 早期版本的Objective-C允许performSelector:(手动内存管理)
  • 带ARC警告的OBJECT-C,用于performSelector:
  • Swift无法访问performSelector:并将这些方法记录为"固有不安全"。
  • 但是,基于命名选择器发送消息的想法并不是"固有的不安全"特性。这个想法已经在Objective-C以及许多其他编程语言中成功地使用了很长一段时间。好的。

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

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

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

    4有关更多详细信息,请参见有关保留返回值和未保留返回值的弧参考。好的。好啊。


    在xcode 4.2中的llvm 3.0编译器中,可以按如下方式取消警告:

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

    如果在多个地方出现错误,并且希望使用C宏系统隐藏pragma,则可以定义一个宏,以便更容易地抑制警告:

    1
    2
    3
    4
    5
    6
    7
    #define SuppressPerformSelectorLeakWarning(Stuff) \
        do { \
            _Pragma("clang diagnostic push") \
            _Pragma("clang diagnostic ignored "-Warc-performSelector-leaks"") \
            Stuff; \
            _Pragma("clang diagnostic pop") \
        } while (0)

    可以这样使用宏:

    1
    2
    3
    SuppressPerformSelectorLeakWarning(
        [_target performSelector:_action withObject:self]
    );

    如果需要执行消息的结果,可以执行以下操作:

    1
    2
    3
    4
    id result;
    SuppressPerformSelectorLeakWarning(
        result = [_target performSelector:_action withObject:self]
    );


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

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

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


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

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


    在编译器允许重写警告之前,您可以使用运行时

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

    而不是

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

    你得去ZZU1[2]


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

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

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


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

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

    这将删除警告,可能是因为它使编译器确信没有对象可以返回,并且以某种方式管理不当。


    下面是基于上述答案的更新宏。这个应用程序应该允许您使用RETURN语句包装代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #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]
    );


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

    1
    2
    3
    4
    5
    6
    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不同,这对任何方法都有效。


    嗯,这里有很多答案,但由于这有点不同,结合了一些答案,我想我会把它放进去。我使用的是nsObject类别,它检查以确保选择器返回void,并抑制编译器警告。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    #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


    为了子孙后代,我决定把帽子扔进戒指里。)

    最近,我看到越来越多的重组远离了targetselector/的模式,有利于协议、块等。但是,我已经用了几次的performSelector替代了一次:

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

    这些似乎是一个干净的,电弧安全,几乎相同的替代品,为performSelector,而不必与objc_msgSend()

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


    Matt Galloway在这条线上的回答解释了原因:

    Consider the following:

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

    Now, how can ARC know that the first returns an object with a retain count of 1 but the second
    returns an object which is autoreleased?

    如果忽略返回值,通常可以安全地抑制警告。如果您真的需要从PerformSelector中获取保留的对象——而不是"不要这样做",那么我不确定最佳实践是什么。


    @C-Road在这里提供了问题描述的正确链接。下面您可以看到我的示例,当PerformSelector导致内存泄漏时。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    @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


    要使Scott Thompson的宏更通用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 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")

    然后这样使用:

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


    不要取消警告!

    对编译器进行修补的替代解决方案不少于12种。虽然您在第一次实现时很聪明,但地球上很少有工程师能跟随您的脚步,而这段代码最终会被破坏。

    安全路线:

    所有这些解决方案都会起作用,与您最初的意图存在一定程度的差异。假设您希望,param可以是nil

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

    1
    2
    3
    4
    5
    6
    // 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]];

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

    (见此回复)使用任何螺纹代替[NSThread mainThread]

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 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]];

    危险路线

    需要某种类型的编译器静默,这必然会中断。请注意,目前,它确实在迅速突破。

    1
    2
    3
    4
    // AT YOUR OWN RISK
    [_controller performSelector:selector];
    [_controller performSelector:selector withObject:anArgument];
    [_controller performSelector:selector withObject:anArgument withObject:nil];


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


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

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

    我将使用nsinvocation,如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
        -(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];
        }

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

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

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

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

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

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

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

    1
    [self.source doSomethingWithObject:object];