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

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

  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 на сохраненные возвращаемые значения и нераспознанные возвращаемые значения для получения более подробной информации.

В компиляторе 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 не может обеспечить надлежащее управление памятью.

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

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

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

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

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

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

вместо

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

Вам придется

#import <objc/message.h>

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

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

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

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

[_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://stackru.com/questions/7017281/performselector-may-cause-a-leak-because-its-selector-is-unknown

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

Похоже, что это чистая, безопасная для ARC и почти идентичная замена performSelector без особого отношения к objc_msgSend(),

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

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

Учтите следующее:

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

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

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

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

@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, вы увидите следующую картинку:... и нет никаких утечек памяти в любом другом случае:

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

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

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

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

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

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

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

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

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

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

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

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

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

[self.source doSomethingWithObject:object];
Другие вопросы по тегам