ios - instruments leaks




由于其选择器未知,performSelector可能会导致泄漏 (13)

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

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

以下是我正在做的事情:

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

为什么我会收到此警告? 我知道编译器不能检查选择器是否存在,但为什么会导致泄漏? 我怎样才能改变我的代码,使我不再接受这个警告?


不要压制警告!

有不少于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]];

危险路线

需要某种编译器的沉默,这一定会中断。 Note that at present time, it did break in Swift .

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

编译器出于某种原因警告这一点。 这种警告很容易被忽略,这很容易解决。 就是这样:

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函数指针。 所有的NSObjectmethodForSelector:响应,但是你也可以在Objective-C运行库中使用class_getMethodImplementation (如果你只有一个协议引用,比如id<SomeProto>id<SomeProto> 。 这些函数指针被称为IMP ,并且是简单的typedef函数指针( id (*IMP)(id, SEL, ...)1 。 这可能接近方法的实际方法签名,但并不总是完全匹配。

一旦你拥有了IMP ,你需要将它转换为一个函数指针,该指针包含ARC需要的所有细节(包括每个Objective-C方法调用的隐含的隐含参数self_cmd )。 这是在第三行中处理的(void *)右边的(void *)只是告诉编译器,你知道你在做什么,不会因为指针类型不匹配而产生警告)。

最后,你调用函数指针2

复杂的例子

当选择器接受参数或返回一个值时,你必须改变一点:

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:的调用methodForSelector:假定它所调用的方法的返回值是一个对象,但不保留/释放它。 所以,如果你的对象应该像上面#3那样被释放(也就是说,你调用的方法返回一个新对象),那么你最终可能会创建一个泄漏。

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

其他参数

一个需要注意的是,这与performSelector:withObject:会发生相同的警告performSelector:withObject:并且您可能会遇到类似的问题,但不会声明该方法如何消耗参数。 ARC允许声明消耗的参数 ,并且如果该方法使用该参数,则最终可能会向僵尸发送消息并崩溃。 有很多方法可以通过桥接模式来解决这个问题,但是真的使用上面的IMP和函数指针方法会更好。 由于消耗的参数很少成为问题,所以不太可能出现。

静态选择器

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

[_controller performSelector:@selector(someMethod)];

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

抑制

在试图想要抑制这种警告是必要的并且良好的代码设计的时候,我会空白。 有人请分享,如果他们有经验沉默这个警告是必要的(和上述不正确处理的事情)。

更多

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

历史

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

随着Swift的推出,Apple将performSelector:方法族记录为“固有不安全”,并且它们不适用于Swift。

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

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

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

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

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

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

4有关更多详细信息,请参阅保留返回值和未保留返回值的ARC参考。


Matt Galloway在这个主题上的回答解释了为什么:

考虑以下:

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

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

如果您忽略了返回值,似乎通常会安全地取消警告。 我不确定最好的做法是什么,如果你真的需要从performSelector获取保留的对象 - 除了“不这样做”。


If you don't need to pass any arguments an easy workaround is to use valueForKeyPath . This is even possible on a Class object.

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

You could also use a protocol here. So, create a protocol like so:

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

In your class that needs to call your selector, you then have a @property.

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

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

[self.source doSomethingWithObject:object];

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

最近我已经看到越来越多的重构远离target / selector范例,支持诸如协议,块等等。然而,现在我已经使用了几次的performSelector替代方案:

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

这些似乎是一个干净的,ARC安全的,几乎相同的替代performSelector而不必与objc_msgSend()多少有关。

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


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

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

代替

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

你必须

#import <objc/message.h>


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

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

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

这可以消除警告,大概是因为它可以让编译器确信没有任何对象可以被返回并以某种方式被错误地管理。


好吧,这里有很多答案,但是因为这有点不同,结合了一些我认为我会介绍的答案。我使用了一个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://.com/questions/7017281/performselector-may-cause-a-leak-because-its-selector-is-unknown

- (void) checkSelector:(SEL)aSelector {
    // See http://.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

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

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


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


这是根据上面给出的答案更新的宏。 这个应该允许你用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]
);




automatic-ref-counting