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


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

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

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

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

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


1185


источник


Ответы:


Решение

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

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>). Эти указатели функций называются IMPs, и просты typedefed указатели функций ( 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

Есть действительно только 4 вещи, которые ARC рассмотрит для возвращаемого значения: 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чтобы справиться с этим, но для этого требуется гораздо больше ввода текста, а также медленнее, поэтому нет оснований для этого.

история

Когда 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 на сохраненные возвращаемые значения а также недопустимые значения возврата Больше подробностей.


1136



В компиляторе 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]
);

1168



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

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

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


206



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

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


119



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

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

вместо

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

Вы должны будете

#import <objc/message.h>


110



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

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

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


87



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

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

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


67



Here is an updated macro based on the answer given above. This one should allow you to wrap your code even with a return statement.

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

34



This code doesn't involve compiler flags or direct runtime calls:

SEL selector = @selector(zeroArgumentMethod);
NSMethodSignature *methodSig = [[self class] instanceMethodSignatureForSelector:selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setSelector:selector];
[invocation setTarget:self];
[invocation invoke];

NSInvocation allows multiple arguments to be set so unlike performSelector this will work on any method.


31



Well, lots of answers here, but since this is a little different, combining a few answers I thought I'd put it in. I'm using an NSObject category which checks to make sure the selector returns void, and also suppresses the compiler warning.

#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

20



For posterity's sake, I've decided to throw my hat into the ring :)

Recently I've been seeing more and more restructuring away from the target/selector paradigm, in favor of things such as protocols, blocks, etc. However, there is one drop-in replacement for performSelector that I've used a few times now:

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

These seem to be a clean, ARC-safe, and nearly identical replacement for performSelector without having to much about with objc_msgSend().

Though, I have no idea if there is an analog available on iOS.


16



Matt Galloway's answer on this thread explains the why:

Consider the following:

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?

It seems that it is generally safe to suppress the warning if you are ignoring the return value. I'm not sure what the best practice is if you really need to get a retained object from performSelector -- other than "don't do that".


15