performSelector может вызвать утечку, потому что его селектор неизвестно

Я получаю следующее предупреждение от компилятора ARC:

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

вот что я делаю:

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

почему я получаю это предупреждение? Я понимаю, что компилятор не может проверить, существует ли селектор или нет, но почему это вызовет утечку? И как я могу изменить свой код, чтобы я больше не получал этого предупреждения?

19 ответов


решение

компилятор предупреждает об этом по какой-то причине. Очень редко это предупреждение следует просто игнорировать,и его легко обойти. Вот как:

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 для метода, соответствующего контроллеру. Все!--4-->s ответить methodForSelector:, но вы также можете использовать class_getMethodImplementation в среде выполнения Objective-C (полезно, если у вас есть только ссылка на протокол, например id<SomeProto>). Эти указатели функций называются IMPs, и просты typedefуказатели функции ed (id (*IMP)(id, SEL, ...))1. Это может быть близко к фактической сигнатуре метода метода, но не всегда будет точно соответствовать.

после IMP, вам нужно привести его к указателю функции, который включает все детали, необходимые ARC (включая два неявных скрытых аргумента self и _cmd каждого вызова метода Objective-C). Это обрабатывается в третьей строке ((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 среда выполнения должна знать, что делать с результатом вызываемого метода. Результатом может быть что угодно: void, int, char, NSString *, id, etc. ARC обычно получает эту информацию из заголовка типа объекта, с которым вы работаете.3

на самом деле есть только 4 вещи, которые ARC будет рассмотрим для возвращаемого значения:4

  1. игнорировать типы не-объектов (void, int, etc)
  2. сохранить значение объекта, а затем отпустить, когда он больше не используется (стандартное предположение)
  3. отпустите новые значения объекта, когда они больше не используются (методы в init/ copy семья или списать с ns_returns_retained)
  4. ничего не делать и предполагать, что возвращаемое значение объекта будет допустимым в локальной области (пока внутренний пул большинства выпусков слить, списать с ns_returns_autoreleased)

вызов methodForSelector: предполагает, что возвращаемое значение вызываемого метода является объектом, но не сохраняет/освобождает его. Таким образом, вы можете создать утечку, если ваш объект должен быть выпущен, как в #3 выше (то есть метод, который вы вызываете, возвращает новый объект).

для селекторов вы пытаетесь вызвать это возвращение void или другие не-объекты, вы можете включить функции компилятора, чтобы игнорировать предупреждение, но это может быть опасно. Я видел, как Clang проходит несколько итераций того, как он обрабатывает возвращаемые значения, которые не назначаются локальным переменным. Нет причин, по которым с включенной ARC он не может сохранить и освободить значение объекта, возвращаемое из methodForSelector: даже если вы не хотите использовать его. С точки зрения компилятора, это все-таки объект. Это означает, что если метод, который вы вызываете,someMethod, возвращает не объект (включая void), вы могли бы в конечном итоге с мусором значение указателя сохраняется/освобождается и аварийно завершает работу.

Дополнительные Параметры

одно соображение заключается в том, что это то же самое предупреждение произойдет с performSelector:withObject: и вы можете столкнуться с аналогичными проблемами, не объявляя, как этот метод потребляет параметры. ARC позволяет объявлять потребляемая параметров, и если метод потребляет параметр, вы, вероятно, в конечном итоге отправить сообщение зомби и аварии. Есть способы обойти это мостовой кастинг, но на самом деле было бы лучше просто использовать IMP и методология указателя функции выше. Поскольку потребляемые параметры редко являются проблемой, это вряд ли возникнет.

Статический Селекторов

интересно, что компилятор не будет жаловаться на селекторы объявлен статически:

[_controller performSelector:@selector(someMethod)];

причина этого в том, что компилятор фактически способен записывать всю информацию о селекторе и объекте во время сборник. Ему не нужно делать никаких предположений. (Я проверил это год назад, посмотрев на источник, но сейчас у меня нет ссылки.)

подавление

пытаясь придумать ситуацию, в которой подавление этого предупреждения было бы необходимым и хорошим дизайном кода, я прихожу к пробелу. Кто-то, пожалуйста, поделитесь, если у них был опыт, когда глушение этого предупреждения было необходимо (и выше не обрабатывает вещи правильно.)

больше

можно создать NSMethodInvocation чтобы справиться с этим, но для этого требуется гораздо больше ввода, а также медленнее, поэтому для этого мало причин.

история

когда performSelector: семейство методов было впервые добавлено к Objective-C, ARC не существовало. При создании ARC Apple решила, что для этих методов следует создать предупреждение, чтобы помочь разработчикам использовать другие средства для явно определите, как должна обрабатываться память при отправке произвольных сообщений через именованный селектор. В Objective-C разработчики могут сделать это, используя слепки стиля C на необработанных указателях функций.

С введением Swift, Apple документально the performSelector: семейство методов как "изначально небезопасных", и они недоступны для Swift.

со временем мы увидели эту прогрессию:

  1. ранние версии Цель-C позволяют performSelector: (ручное управление памятью)
  2. Objective-C с ARC предупреждает об использовании performSelector:
  3. Swift не имеет доступа к performSelector: и документирует эти методы как "изначально небезопасные"

идея отправки сообщений на основе именованного селектора не является, однако, "изначально небезопасной" функцией. Эта идея уже давно успешно используется в Objective-C, а также во многих других программах языки.


1 все методы Objective-C имеют два скрытых аргумента,self и _cmd, которые добавляются при вызове метода.

2 вызов NULL функция небезопасна в C. охранник, используемый для проверки наличия контроллера, гарантирует, что у нас есть объект. Поэтому мы знаем, что получим IMP С methodForSelector: (хотя это может быть _objc_msgForward, вход в систему пересылки сообщений). В принципе, с охранником на месте, мы знаем, что у нас есть функция вызова.

3 на самом деле, это возможно для него, чтобы получить неправильную информацию, если объявить вам объекты как id и вы не импортируете все заголовки. Вы можете столкнуться с сбоями в коде, который компилятор считает прекрасным. Это очень редко, но может случиться. Обычно вы просто получаете предупреждение о том, что он не знает, какую из двух сигнатур метода выбрать.

4 посмотреть Ссылка ARC на сохранить возвращаемые значения и распределенная возвращаемые значения для получения более подробной информации.


в компиляторе LLVM 3.0 в Xcode 4.2 вы можете подавить предупреждение следующим образом:

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

мое предположение об этом таково: поскольку селектор неизвестен компилятору, ARC не может обеспечить надлежащее управление памятью.

на самом деле, бывают случаи, когда управление памятью привязано к имени метода определенным соглашением. В частности, я думаю о удобство конструкторов и сделать методы; возвращения к Конвенции объект autoreleased; последний сохраненный объект. Конвенция основана на названиях selector, поэтому, если компилятор не знает селектор, то он не может применить правильное правило управления памятью.

Если это правильно, я думаю, что вы можете безопасно использовать свой код, если вы убедитесь, что все в порядке с управлением памятью (например, что ваши методы не возвращают объекты, которые они выделяют).


в проекте Параметры Построения, под Другие Предупреждающие Флаги (WARNING_CFLAGS), добавить
-Wno-arc-performSelector-leaks

теперь просто убедитесь, что вызываемый селектор не вызывает сохранение или копирование объекта.


в качестве обходного пути, пока компилятор не позволит переопределить предупреждение, вы можете использовать runtime

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

вместо

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

вы должны

#import <objc/message.h>

чтобы игнорировать ошибку только в файле с селектором perform, добавьте # pragma следующим образом:

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

это проигнорирует предупреждение в этой строке, но все равно позволит ему на протяжении всей остальной части вашего проекта.


странно, но верно: если приемлемо (т. е. результат пуст, и вы не против позволить циклу runloop один раз), добавьте задержку, даже если это ноль:

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

Это удаляет предупреждение, предположительно потому, что это гарантирует компилятору, что объект не может быть возвращен, и как-то неумело.


вот обновленный макрос, основанный на ответе, приведенном выше. Это должно позволить вам обернуть свой код даже с помощью оператора 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]
);

этот код не включает флаги компилятора или прямые вызовы среды выполнения:

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, а также подавляет предупреждение компилятора.

#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

для потомков, я решил бросить свою шляпу на ринг :)

в последнее время я вижу все больше и больше реструктуризации от target/selector парадигма, в пользу таких вещей, как протоколы, блоки и т. д. Тем не менее, есть одна замена для performSelector что я использовал несколько раз:

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

это, кажется, быть чистой, дуги безопасности, и почти идентичной замены performSelector не много о с objc_msgSend().

хотя я понятия не имею, есть ли аналог на iOS.


ответ Мэтта Гэллоуэя на этой теме объясняю почему:

рассмотрим следующее:

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

теперь, как ARC может знать, что первый возвращает объект с числом сохранения 1, но второй возвращает объект, который является autoreleased?

Кажется, что обычно безопасно подавлять предупреждение, если вы игнорируете возвращаемое значение. Я не уверен, что лучшая практика, если вам действительно нужно получить сохраненный объект из performSelector -- кроме "не делайте этого".


@c-road обеспечивает правильную связь с описанием проблемы здесь. Ниже вы можете увидеть мой пример, когда 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


чтобы сделать макрос Скотта Томпсона более общим:

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

поскольку вы используете ARC, вы должны использовать iOS 4.0 или более позднюю версию. Это означает, что вы можете использовать блоки. Если вместо запоминания селектора для выполнения вы вместо этого взяли блок, ARC сможет лучше отслеживать, что на самом деле происходит, и вам не придется рисковать случайным введением утечки памяти.


не подавлять предупреждения!

не менее 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]];

Безопасный маршрут, немного другое поведение:

(см. этой ответ)
Используйте любой поток вместо [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]];

Опасные Маршруты

требует какого-то молчания компилятора, которое обязательно сломается. Отметим, что в настоящее время, это сделал перерыв Свифт.

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

вместо использования блочного подхода, который дал мне некоторые проблемы:

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

Если вам не нужно передавать какие-либо аргументы, простым обходным путем является использование valueForKeyPath. Это даже возможно на


вы также можете использовать протокол здесь. Итак, создайте такой протокол:

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

в вашем классе, который должен вызвать ваш селектор, у вас есть свойство@.

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

когда вам нужно позвонить @selector(doSomethingWithObject:) в экземпляре MyObject сделайте следующее:

[self.source doSomethingWithObject:object];