executeSelector может вызвать утечку, потому что его селектор неизвестен - PullRequest
1238 голосов
/ 11 августа 2011

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

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

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

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

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

Ответы [ 19 ]

1189 голосов
/ 19 ноября 2013

Решение

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

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 для метода, соответствующего контроллеру. Все NSObject отвечают на methodForSelector:, но вы также можете использовать class_getMethodImplementation во время выполнения Objective-C (полезно, если у вас есть только ссылка на протокол, например id<SomeProto>). Эти указатели функций называются IMP s и являются простыми 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 и т. Д. ARC обычно получает эту информацию из заголовка типа объекта, с которым вы работаете. 3

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

  1. Игнорировать необъектные типы (void, int и т. Д.)
  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 длясправиться и с этим, но это требует гораздо большего набора текста, а также медленнее, поэтому нет особых причин делать это.

History

Когда впервые было добавлено семейство методов 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 Вызов функции NULL небезопасен в C. Охрана, используемая для проверки наличияКонтроллер гарантирует, что у нас есть объект.Поэтому мы знаем, что получим IMP от methodForSelector: (хотя это может быть _objc_msgForward, вход в систему пересылки сообщений).По сути, с установленной защитой мы знаем, что у нас есть функция для вызова.

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

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

1178 голосов
/ 28 октября 2011

В компиляторе 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]
);
208 голосов
/ 11 августа 2011

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

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

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

120 голосов
/ 31 октября 2011

В вашем проекте Настройки сборки , под Другие флажки предупреждения (WARNING_CFLAGS), добавьте
-Wno-arc-performSelector-leaks

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

111 голосов
/ 16 августа 2011

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

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

вместо

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

Вам придется

#import <objc/message.h>
88 голосов
/ 19 января 2012

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

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

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

68 голосов
/ 11 ноября 2012

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

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

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

34 голосов
/ 07 мая 2013

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

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

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 это будет работать для любого метода.

20 голосов
/ 21 февраля 2014

Ну, здесь много ответов, но поскольку это немного по-другому, объединяя несколько ответов, я думал, что вставлю это. Я использую категорию 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 /4844809/executeselector-mozhet-vyzvat-utechku-potomu-chto-ego-selektor-neizvesten

- (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
...