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




objective-c memory-leaks (13)

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

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

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

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

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


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

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

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

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

Решение

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

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 s отвечают на methodForSelector: но вы также можете использовать class_getMethodImplementation в 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

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

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

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

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

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

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

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

[_controller performSelector:@selector(someMethod)];

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

подавление

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

Больше

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

история

Когда в Objective-C впервые было добавлено семейство методов performSelector: 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 по сохраненным значениям возврата и невыполненным значениям возврата .


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

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


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

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

вместо

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

Вам придется

#import <objc/message.h>


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

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

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

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

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

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


Ну, здесь много ответов, но, поскольку это немного отличается, объединив несколько ответов, я думал, что я их вложу. Я использую категорию 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://.com/questions/7017281/performselector-may-cause-a-leak-because-its-selector-is-unknown

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

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

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

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

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

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


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

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

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


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

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

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


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

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

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


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

    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. Это возможно даже для Classобъекта.

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




automatic-ref-counting