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


1258

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

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

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

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

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


3
Имя переменной является динамическим, оно зависит от многих других вещей. Есть риск, что я назову что-то, чего не существует, но это не проблема.
Эдуардо Скос

6
@matt, почему вызов метода динамически на объекте был бы плохой практикой? Разве не вся цель NSSelectorFromString () для поддержки этой практики?
Эдуардо Скос

7
Вы должны / могли бы также протестировать [_controller responsedsToSelector: mySelector] перед установкой его с помощью executeSelector:
mattacular

50
@mattacular Жаль, что я не мог бы голосовать: "Это ... это плохая практика".
ctpenrose

6
Если вы знаете, что строка является литералом, просто используйте @selector (), чтобы компилятор мог определить, как называется селектор. Если ваш реальный код вызывает NSSelectorFromString () со строкой, созданной или предоставленной во время выполнения, то вы должны использовать NSSelectorFromString ().
Крис Пейдж

Ответы:


1212

Решение

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

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


@wbyoung Если ваш код решает проблему сохранения, мне интересно, почему performSelector:методы не реализованы таким образом. Они имеют строгую сигнатуру метода (возврат id, взятие одного или двух idс), поэтому не нужно обрабатывать примитивные типы.
Tricertops

1
@ И аргумент обрабатывается на основе определения прототипа метода (он не будет сохранен / освобожден). Проблема в основном основана на типе возвращаемого значения.
wbyoung

2
«Сложный пример» выдает ошибку Cannot initialize a variable of type 'CGRect (*)(__strong id, SEL, CGRect, UIView *__strong)' with an rvalue of type 'void *'при использовании последней версии Xcode. (5.1.1) Тем не менее, я многому научился!
Стэн Джеймс

2
void (*func)(id, SEL) = (void *)imp;не компилируется, я заменил его наvoid (*func)(id, SEL) = (void (*)(id, SEL))imp;
Davyd Geyl

1
изменить void (*func)(id, SEL) = (void *)imp;на <…> = (void (*))imp;или<…> = (void (*) (id, SEL))imp;
Исаак Осипович Дунаевский

1183

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

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

4
@Eric Нет, это невозможно, если только вы не вызываете забавные методы, такие как «initSomething», «newSomething» или «thingCopy ».
Андрей Таранцов

3
@Julian Это работает, но отключает предупреждение для всего файла - это может не понадобиться или вам не нужно. Wrappping его с popи push-pragmas гораздо чище и безопаснее.
Эмиль

2
Все, что это делает, это заставляет компилятор замолчать. Это не решает проблему. Если селектор не существует, вы в значительной степени облажались.
Андра Тодореску

2
Это следует использовать только в том случае, если оно заключено в if ([_target respondsToSelector:_selector]) {аналогичную логику.

208

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

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

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


5
Спасибо за ответ, я посмотрю больше на это, чтобы увидеть, что происходит. Любая идея о том, как я могу обойти предупреждение и заставить его исчезнуть? Я бы не хотел, чтобы предупреждение всегда оставалось в моем коде для безопасного вызова.
Эдуардо Скос

84
Так что я получил подтверждение от кого-то из Apple на их форумах, что это действительно так. Они будут добавлять забытое переопределение, чтобы позволить людям отключить это предупреждение в будущих выпусках. Спасибо.
Эдуардо Скос

5
Этот ответ поднимает некоторые вопросы, например, если ARC пытается определить, когда выпускать что-либо на основе соглашения и имен методов, то как это «подсчет ссылок»? Поведение, которое вы описываете, звучит лишь немного лучше, чем полностью произвольное, если ARC предполагает, что код следует определенному соглашению, а не фактически отслеживает ссылки, независимо от того, какое соглашение соблюдается.
aroth

8
ARC автоматизирует процесс добавления сохранений и выпусков при компиляции. Это не сборка мусора (поэтому он невероятно быстрый и требует минимальных затрат). Это совсем не произвольно. Правила по умолчанию основаны на устоявшихся соглашениях ObjC, которые последовательно применялись в течение десятилетий. Это избавляет от необходимости явно добавлять __attributeк каждому методу объяснение управления его памятью. Но это также делает невозможным для обработчика правильно обрабатывать этот шаблон (шаблон, который раньше был очень распространенным, но в последние годы был заменен на более устойчивые шаблоны).
Роб Нейпир

8
То есть мы больше не можем иметь тип ивара SELи назначать разные селекторы в зависимости от ситуации? Путь, динамичный язык ...
Николас Миари

121

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

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


12
Обратите внимание, что вы можете добавить один и тот же флаг для определенных файлов, а не для всего проекта. Если вы посмотрите под Build Phases-> Compile Sources, вы можете установить для каждого файла флаги компилятора (так же, как вы хотите сделать для исключения файлов из ARC). В моем проекте только один файл должен использовать селекторы таким образом, поэтому я просто исключил его и оставил остальные.
Майкл

111

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

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

вместо

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

Вам придется

#import <objc/message.h>


8
ARC распознает соглашения по какао, а затем добавляет удержания и выпуски на основе этих соглашений. Поскольку C не следует этим соглашениям, ARC заставляет вас использовать методы ручного управления памятью. Если вы создаете объект CF, вы должны CFRelease () его. Если вы dispatch_queue_create (), вы должны dispatch_release (). В итоге, если вы хотите избежать предупреждений ARC, вы можете избежать их, используя объекты C и ручное управление памятью. Кроме того, вы можете отключить ARC для каждого файла, используя флаг компилятора -fno-objc-arc в этом файле.
jluckyiv

8
Не без кастинга, ты не можешь. Varargs - это не то же самое, что явно заданный список аргументов. Обычно это работает по совпадению, но я не считаю «по совпадению» правильным.
bbum

21
Не делайте этого [_controller performSelector:NSSelectorFromString(@"someMethod")];и objc_msgSend(_controller, NSSelectorFromString(@"someMethod"));не эквивалентны! Взгляните на Несоответствия сигнатур методов и Большой недостаток слабой типизации в Objective-C, они объясняют проблему в деталях.
0xced

5
@ 0xced В этом случае все нормально. objc_msgSend не создаст несоответствие сигнатуры метода для любого селектора, который работал бы правильно в executeSelector: или его вариантах, поскольку они только когда-либо принимают объекты в качестве параметров. Пока все ваши параметры - это указатели (включая объекты), doubles и NSInteger / long, а ваш тип возвращаемого значения void, pointer или long, objc_msgSend будет работать правильно.
Мэтт Галлахер

88

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

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

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


6
Я понимаю, что вы также можете включить предупреждение сразу же после того, как этот метод с помощью #pragma clang diagnostic warning "-Warc-performSelector-leaks". Я знаю, что если я отключу предупреждение, мне нравится включать его снова в кратчайшие возможные сроки, чтобы я случайно не пропустил другое непредвиденное предупреждение. Вряд ли это проблема, но это просто моя практика, когда я отключаю предупреждение.
Роб

2
Вы также можете восстановить предыдущее состояние конфигурации компилятора, используя #pragma clang diagnostic warning pushперед внесением каких-либо изменений и #pragma clang diagnostic warning popвосстановить предыдущее состояние. Полезно, если вы отключаете нагрузку и не хотите, чтобы в вашем коде было много повторных включений прагматических строк.
ДинВомбурн

Это будет игнорировать только следующую строку?
hfossli

70

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

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

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


2
Знаете ли вы, действительно ли это решает связанные проблемы управления памятью, или у него те же проблемы, но XCode не достаточно умен, чтобы предупредить вас этим кодом?
Аарон Брагер,

Это семантически не то же самое! Использование executeSelector: withObject: AfterDelay: выполнит селектор при следующем запуске цикла запуска. Следовательно, этот метод возвращается немедленно.
Флориан,

10
@Florian Конечно, это не то же самое! Прочитайте мой ответ: я говорю, если это приемлемо, потому что результат пуст и циклы runloop. Это первое предложение моего ответа.
матовый

34

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

6
returnне должен быть внутри макроса; return SUPPRESS_PERFORM_SELECTOR_LEAK_WARNING([_target performSelector:_action withObject:self]);также работает и выглядит более разумным.
UASI

31

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

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


3
Знаете ли вы, действительно ли это решает связанные проблемы управления памятью, или у него те же проблемы, но XCode не достаточно умен, чтобы предупредить вас этим кодом?
Аарон Брагер,

1
Можно сказать, что это решает проблемы управления памятью; но это потому, что это в основном позволяет вам указать поведение. Например, вы можете разрешить вызову сохранять аргументы или нет. Насколько мне известно, он пытается исправить проблемы с несоответствием сигнатур, которые могут возникнуть, полагая, что вы знаете, что делаете, и не предоставляете ему неверные данные. Я не уверен, что все проверки могут быть выполнены во время выполнения. Как упоминается в другом комментарии, mikeash.com/pyblog/… хорошо объясняет, что могут сделать несоответствия.
Михай Тимар

20

Ну, здесь много ответов, но поскольку это немного отличается, объединяя несколько ответов, я думал, что вставлю это. Я использую категорию 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://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

Следует ли заменить 'v' на _C_VOID? _C_VOID объявлен в <objc / runtime.h>.
Рик Ренич

16

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

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

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

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

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


6
Спасибо за включение этого .. Он доступен в прошивкой: [[UIApplication sharedApplication] sendAction: to: from: forEvent:]. Однажды я посмотрел на нее, но было бы неудобно использовать связанный с пользовательским интерфейсом класс в середине вашего домена или службы просто для выполнения динамического вызова. Хотя, спасибо, что включили это!
Эдуардо Скос

2
Еа! У него будет больше накладных расходов (поскольку ему нужно проверить, доступен ли метод и пройтись по цепочке респондента, если он не существует), и иметь другое поведение при ошибках (пройти по цепочке респондента и вернуть NO, если он ничего не может найти который отвечает на метод, а не просто сбой). Это также не работает, когда вы хотите idот-performSelector:...
tc.

2
@tc. Он не «идет по цепочке респондента», если не to:равен нулю, а это не так. Он просто идет прямо к целевому объекту без предварительной проверки. Так что нет «больше накладных расходов». Это не очень хорошее решение, но причина, которую вы приводите, не является причиной. :)
Мэтт

15

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

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

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

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

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


14

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


6

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

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

FWIW, я не добавил макрос. Кто-то добавил это к моему ответу. Лично я бы не использовал макрос. Прагма предназначена для работы с особым случаем в коде, и прагмы очень четкие и прямые о том, что происходит. Я предпочитаю держать их на месте, а не прятать или абстрагировать их от макроса, но это только я. YMMV.
Скотт Томпсон

@ ScottThompson Это честно. Для меня легко найти этот макрос по всей моей кодовой базе, и я, как правило, также добавляю предупреждение без вывода сообщений для решения основной проблемы.
Бен Флинн

6

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

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

3
Формулировка очень неправильная. Безопасные маршруты вовсе не безопаснее, чем опасны. Это, возможно, более опасно, потому что оно скрывает предупреждение неявно.
Брайан Чен,

Я исправлю формулировку, чтобы не оскорблять, но я верю своему слову. Единственное время, когда я нахожу предупреждение о глушении, это если я не владею кодом. Ни один инженер не может безопасно поддерживать код без вывода сообщений, не понимая всех последствий, которые могут означать чтение этого аргумента, и эта практика является довольно рискованной; особенно если вы рассматриваете 12, простой английский, надежные альтернативы.
SwiftArchitect

1
Нет, ты не понял мою точку зрения. Используя performSelectorOnMainThreadэто не хороший способ , чтобы заставить замолчать предупреждение и имеют побочные эффекты. (это не устраняет утечку памяти). Дополнительный #clang diagnostic ignored явно подавляет предупреждение очень ясным способом.
Брайан Чен

Верно, что выполнение селектора для не - (void)метода - это реальная проблема.
SwiftArchitect

и как вы вызываете селектор с несколькими аргументами через это и быть безопасным в то же время? @SwiftArchitect
Catalin

4

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


На самом деле, блоки позволяют очень легко случайно создать цикл сохранения, который ARC не решает. Я все еще хотел бы, чтобы при компиляции selfчерез ivar было предупреждение о компиляторе (например, ivarвместо self->ivar).
тк.

Ты имеешь в виду как -Wimplicit-retain-self?
OrangeDog

2

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

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

1

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

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

-2

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

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

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

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

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

[self.source doSomethingWithObject:object];

2
Привет, Ву, спасибо, но смысл использования NSSelectorFromString в том, что вы не знаете, какой селектор вы хотите вызвать во время выполнения.
Эдуардо Скос
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.