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

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

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

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

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

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

Ответ 1

Решение

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

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

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

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

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

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

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

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

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

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

[_controller performSelector:@selector(someMethod)];

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

Подавление

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

Подробнее

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

История

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

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

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

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

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


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

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

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

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

Ответ 2

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

Ответ 3

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

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

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

Ответ 4

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

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

Ответ 5

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

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

вместо

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

Вам нужно

#import <objc/message.h>

Ответ 6

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

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

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

Ответ 7

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

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

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

Ответ 8

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

Ответ 9

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

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

Ответ 10

Ну, здесь много ответов, но, поскольку это немного отличается, объединив несколько ответов, я думал, что я их вложу. Я использую категорию NSObject, которая проверяет, чтобы селектор вернул пустоту, а также подавляет предупреждение компилятора.

#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

Ответ 11

Для потомков я решил бросить свою шляпу в кольцо:)

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

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

Кажется, что это чистая, ARC-безопасная и почти идентичная замена для performSelector, не имея большого значения с objc_msgSend().

Хотя, я понятия не имею, есть ли аналог, доступный на iOS.

Ответ 12

Ответ Matt Galloway на этот поток объясняет, почему:

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

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

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

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

Ответ 13

@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 возвращает сохраненный объект.

Если вы запустите инструмент Memory Leak Tool, вы увидите следующее изображение: enter image description here ... и нет утечек памяти в любом другом случае: enter image description here

Ответ 14

Сделать макрос Скотта Томсона более универсальным:

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

Ответ 15

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

Ответ 16

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

Существует не менее 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]];

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

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

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

Ответ 17

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

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

Ответ 18

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

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

Ответ 19

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

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

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

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

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

[self.source doSomethingWithObject:object];

Ответ 20

Есть еще один способ обойти это предупреждение.

<del>[instanceSelector performSelector:stopSelector];</del>

Использовать метод overDelay overloaded

[instanceSelector performSelector:stopSelector withObject:self afterDelay:0.0];