Не стал ли шаблон проектирования Target-Action плохой практикой в ​​рамках ARC?

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

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

Вот пример:

- (void)itemLoaded {
    [specifiedReceiver performSelector:specifiedSelector];
}

При ARC теперь выясняется, что делать что-то подобное внезапно стало опасно.

Xcode выдает предупреждение, которое выглядит так:

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

Конечно, селектор неизвестен, так как в рамках шаблона разработки Target-Action вы можете указать любой селектор, который вы хотите, чтобы получать вызов, когда происходит что-то интересное.

Что меня больше всего беспокоит в этом предупреждении, так это то, что в нем может быть утечка памяти. Насколько я понимаю, ARC не изменяет правила управления памятью, а просто автоматизирует вставку сообщений retain/release/autorelease в нужные места.

Еще одна вещь, чтобы отметить здесь: -performSelector: имеет id возвращаемое значение ARC анализирует сигнатуры методов, чтобы выяснить, применяя соглашения об именах, если метод возвращает +1 элемент сохранения количества или нет. В этом случае ARC не знает, является ли селектор -newFooBar фабрика или просто вызов незаметного рабочего метода (что в любом случае почти всегда имеет место с Target-Action). На самом деле ARC должен был признать, что я не ожидаю возвращаемого значения, и, следовательно, забыть о любом потенциальном +1 с сохранением возвращаемого значения. Глядя на это с этой точки зрения, я вижу, откуда взялась ARC, но все же существует слишком большая неопределенность относительно того, что это на самом деле означает на практике.

Означает ли это, что в рамках ARC что-то может пойти не так, как без ARC? Я не понимаю, как это может привести к утечке памяти. Может ли кто-нибудь привести примеры ситуаций, в которых это опасно, и как именно в этом случае возникает утечка?

Я чертовски погуглил в интернете, но не нашел ни одного сайта, объясняющего почему.

4 ответа

Решение

Проблема с performSelector является то, что ARC не знает, что делает селектор, который будет выполняться. Учтите следующее:

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

Теперь, как ARC может знать, что первый возвращает объект с счетом сохранения 1, а второй возвращает объект, который автоматически высвобождается? (Я просто определяю метод с именем giveMeAnotherNonRetainedObject здесь, который возвращает что-то авто-релиз). Если это не добавило ни в какие выпуски тогда anotherObject1 протекает здесь.

Очевидно, что в моем примере селекторы, которые должны быть выполнены, на самом деле известны, но представьте, что они были выбраны во время выполнения. ARC действительно не могла сделать свою работу, поставив нужное количество retainс или releaseздесь, потому что он просто не знает, что будет делать селектор. Вы правы в том, что ARC не нарушает никаких правил и просто добавляет правильные вызовы управления памятью для вас, но это именно то, чего он не может здесь сделать.

Вы правы, что тот факт, что вы игнорируете возвращаемое значение, означает, что все будет хорошо, но в целом ARC просто привередлив и предупреждает. Но я думаю, именно поэтому это предупреждение, а не ошибка.

Редактировать:

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

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[specifiedReceiver performSelector:specifiedSelector];
#pragma clang diagnostic pop

Предупреждение должно звучать так:

PerformSelector может вызвать утечку, потому что его селектор неизвестен. ARC не знает, есть ли у возвращенного идентификатора счет сохранения +1 или нет, и поэтому не может должным образом управлять памятью возвращаемого объекта.

К сожалению, это только первое предложение.

Теперь решение:

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

NSArray *linkedNodes = [startNode performSelector:nodesArrayAccessor];

Ваша лучшая ставка заключается в следующем:

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
NSArray *linkedNodes = [startNode performSelector:nodesArrayAccessor];
#pragma clang diagnostic pop

То же самое касается случая в моем первоначальном вопросе, где я полностью игнорирую возвращаемое значение. ARC должен быть достаточно умен, чтобы понять, что меня не волнует возвращаемый идентификатор, и поэтому почти гарантированно, что анонимный селектор не будет фабричным, вспомогательным конструктором или чем-то еще. К сожалению, ARC нет, поэтому применяется то же правило. Не обращайте внимания на предупреждение.

Это также можно сделать для всего проекта, установив флаг компилятора -Wno-arc-executeSelector-leaks в разделе "Другие предупреждающие флаги" в настройках сборки проекта.

Кроме того, вы можете отключить предупреждение для каждого файла отдельно, добавив этот флажок в поле "Ваша цель" > "Фазы сборки" > "Компилировать источники" справа от нужного файла.

Все три решения очень беспорядочные ИМХО, поэтому я надеюсь, что кто-то придумает лучшее.

Как описано выше, вы получаете это предупреждение, потому что компилятор не знает, куда (или если) поместить сохранение / освобождение возвращаемого значения executeSelector:.

Но учтите, что если вы используете [someObject performSelector:@selector(selectorName)] это не будет генерировать предупреждения (по крайней мере в Xcode 4.5 с llvm 4.1), потому что точный селектор легко определить (вы устанавливаете его явно), и именно поэтому компилятор может поместить сохранение / выпуски в правильное место.

Вот почему вы получите предупреждение только в том случае, если вы передадите селектор с помощью указателя SEL, потому что в этом случае компилятор не сможет определить, что делать. Таким образом, используя следующее

SEL s = nil;
if(condition1) SEL = @selector(sel1)
else SEL = @selector(sel2)

[self performSelector:s];

выдаст предупреждение. Но рефакторинг это будет:

if(condition1) [self performSelector:@selector(sel1)]
else [self performSelector:@selector(sel2)]

не будет генерировать никаких предупреждений

ARC выдает предупреждение, потому что не может гарантировать, что селектор не создает объект, о котором он не знает. Теоретически вы можете получить что-то от этого метода, которое ARC не может обработать:

id objectA = [someObject performSelector:@selector(createObjectA)];

Может быть, когда-нибудь это может, но сейчас это не может. (Обратите внимание, что если он знает объект (это не идентификатор), он не выдает это предупреждение).

Если вы пытаетесь просто выполнить метод без получения от него объекта, я рекомендую использовать objc_msgSend. Но вы должны включить в свой класс:

#include <objc/message.h>
objc_msgSend(someObject, action);
Другие вопросы по тегам