performSelector може спричинити протікання, оскільки його селектор невідомий


1258

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

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

Ось що я роблю:

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

Чому я отримую це попередження? Я розумію, що компілятор не може перевірити, чи існує селектор чи ні, але чому це може спричинити витік? І як я можу змінити свій код, щоб більше не отримувати це попередження?


3
Назва змінної є динамічною, це залежить від багатьох інших речей. Існує ризик, що я зателефоную щось, що не існує, але це не проблема.
Едуардо Скоц

6
@matt чому б динамічний виклик методу на об'єкт був поганою практикою? Чи не вся мета NSSelectorFromString () підтримувати цю практику?
Едуардо Скоц

7
Ви повинні / могли також протестувати [_controller respotsToSelector: mySelector] перед тим, як встановити його через performSelector:
mattacular

50
@mattacular Бажаю, я можу проголосувати "Це ... погана практика".
ctpenrose

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

Відповіді:


1211

Рішення

Компілятор попереджає про це з причини. Дуже рідко таке попередження потрібно просто ігнорувати, і його легко обійти. Ось як:

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покажчики функцій ( 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чи інші не об’єкти, ви можете дозволити функціям компілятора ігнорувати попередження, але це може бути небезпечно. Я бачив, як Кланг пройшов кілька повторень того, як він обробляє повернені значення, які не присвоєні локальним змінним. Немає жодної причини, якщо 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
@Andy аргумент обробляється на основі визначення прототипу методу (він не буде збережений / випущений). Занепокоєння здебільшого грунтується на типі повернення.
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;
Давид Гейл

1
зміна void (*func)(id, SEL) = (void *)imp;на <…> = (void (*))imp;або<…> = (void (*) (id, SEL))imp;
Ісаак Осипович Дунаєвський

1182

У компіляторі 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" або "somethingCopy".
Андрій Таранцов

3
@Julian Це працює, але це вимикає попередження для всього файлу - можливо, вам це не потрібно чи потрібно. Обгортання його popта push-прагмами набагато чистіше та безпечніше.
Еміль

2
Все це робить, це замовчує компілятор. Це не вирішує проблему. Якщо селектора немає, ви дуже сильно накручені.
Андра Тодореску

2
Це слід використовувати лише тоді, коли їх обробляють за if ([_target respondsToSelector:_selector]) {подібною логікою.

208

Я здогадуюсь про це так: оскільки селектор невідомий компілятору, ARC не може застосувати належне управління пам'яттю.

Насправді, бувають випадки, коли управління пам’яттю прив’язується до імені методу певним умовою. Зокрема, я думаю про зручність конструкторів, а не про методи; колишнє повернення за умовою повернення об'єкта, що вийшов з огляду останній - об'єкт, що утримується. Конвенція заснована на іменах селектора, тому якщо компілятор не знає селектора, він не може застосувати правильне правило управління пам'яттю.

Якщо це правильно, я вважаю, що ви можете сміливо використовувати свій код, якщо переконайтеся, що все в порядку щодо управління пам'яттю (наприклад, що ваші методи не повертають виділені ними об'єкти).


5
Дякую за відповідь, я детальніше вивчу це, щоб побачити, що відбувається. Будь-яка ідея про те, як я можу обійти попередження і змусити його зникнути? Мені б не хотілося, щоб попередження сиділо в моєму коді назавжди щодо безпечного дзвінка.
Едуардо Скоц

84
Тому я отримав підтвердження від когось із Apple на їхніх форумах, що це дійсно так. Вони додадуть забуте переопрацювання, щоб люди могли відключити це попередження у майбутніх випусках. Дякую.
Едуардо Скоц

5
Ця відповідь викликає деякі питання, наприклад, якщо ARC намагається визначити, коли випустити щось на основі конвенцій та назв методів, то як це "підрахунок посилань"? Поведінка, яку ви описуєте, звучить лише незначно краще, ніж абсолютно довільно, якщо ARC припускає, що код дотримується певної конвенції на відміну від фактичного відстеження посилань незалежно від того, якої конвенції дотримуються.
aroth

8
ARC автоматизує процес додавання утримує та випускає при компіляції. Це не збирання сміття (саме тому це так неймовірно швидко і з низькими накладними витратами). Це зовсім не довільно. Правила за замовчуванням базуються на усталених конвенціях ObjC, які послідовно застосовуються десятиліттями. Це дозволяє уникнути необхідності явно додавати __attributeдо кожного методу, що пояснює його управління пам'яттю. Але це також унеможливлює належне поводження учасника з цією схемою (модель, яка раніше була дуже поширеною, але в останні роки була замінена більш стійкими моделями).
Роб Нап'єр

8
Тож ми не можемо більше мати тип ivar SELта призначати різних селекторів залежно від ситуації? Шлях, динамічна мова ...
Nicolas Miari

121

У проекті Налаштування збірки в розділі Інші попереджувальні прапори ( WARNING_CFLAGS) додайте
-Wno-arc-performSelector-leaks

Тепер просто переконайтесь, що селектор, який ви телефонуєте, не спричиняє збереження або копіювання об'єкта.


12
Зверніть увагу, ви можете додати той самий прапор для конкретних файлів, а не для всього проекту. Якщо ви подивитесь на Фази збірки-> Джерела компіляції, ви можете встановити для кожного файлу прапорці компілятора (так само, як ви хочете зробити для виключення файлів з 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 не створить невідповідність підпису методу жодному селектору, який би працював правильно у performSelector: або його варіантах, оскільки вони лише коли-небудь приймають об'єкти як параметри. Поки всі ваші параметри - покажчики (включаючи об'єкти), подвійні та NSInteger / 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відновлення попереднього стану. Корисно, якщо ви вимикаєте навантаження та не хочете, щоб у вашому коді було багато повторних включень прагматичних рядків.
deanWombourne

Він буде ігнорувати лише наступний рядок?
hfossli

70

Дивно, але правда: якщо прийнятний (тобто результат недійсний, і ви не заперечуєте пускати цикл запуску один раз), додайте затримку, навіть якщо це нуль:

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

Це видаляє попередження, імовірно, тому, що воно запевняє компілятора, що жоден об'єкт не може бути повернутий і якимось чином керований.


2
Чи знаєте ви, чи це насправді вирішує пов'язані з цим питаннями управління пам’яттю, чи це ті самі проблеми, але Xcode недостатньо розумний, щоб попереджати вас цим кодом?
Аарон Брагер

Це семантично не те саме! Використання performSelector: withObject: AfterDelay: виконає селектор у наступному запуску runloop. Тому цей метод повертається негайно.
Флоріан

10
@Florian Звичайно, це не те саме! Прочитайте мою відповідь: я кажу, якщо це прийнятно, тому що результат недійсний і цикл запуску. Це перше речення моєї відповіді.
мат

34

Ось оновлений макрос на основі відповіді, наведеної вище. Цей повинен дозволяти вам обернути свій код навіть із заявою про повернення.

#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/… чудово пояснює, що невідповідності можна зробити.
Mihai Timar

20

Ну, тут багато відповідей, але оскільки це трохи інше, поєднуючи декілька відповідей, я думав, що я його викладу. Я використовую категорію 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

Чи слід "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
Дякуємо, що включили це .. Він доступний в iOS : [[UIApplication sharedApplication] sendAction: to: from: forEvent:]. Я заглянув у це один раз, але мені стає незручно використовувати клас, пов’язаний з інтерфейсом користувача, посеред вашого домену чи послуги просто для того, щоб зробити динамічний дзвінок. Дякуємо за включення цього!
Едуардо Скоц

2
Ой! Він матиме більше накладних витрат (оскільки він повинен перевірити, чи є метод доступним, і перейти на ланцюжок відповідей, якщо його немає) та мати іншу поведінку помилок (піднімаючи ланцюжок відповідей і повертаючи НІ, якщо він нічого не може знайти який відповідає методу, а не просто виходить з ладу). Він також не працює, коли ви хочете idвід-performSelector:...
tc.

2
@tc. Він не "йде вгору по ланцюгу відповідей", якщо не to:буде нуля, чого не є. Він просто прямує до цільового об'єкта без попередньої перевірки. Так що немає "більше накладних витрат". Це не чудове рішення, але причина, яку ви даєте, не є причиною. :)
мат

15

Відповідь Метта Галлоуей на цю тему пояснює, чому:

Розглянемо наступне:

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

Тепер, як ARC може знати, що перший повертає об'єкт із кількістю затримок 1, а другий повертає об'єкт, який автоматично випускається?

Здається, загалом безпечно придушити попередження, якщо ви ігноруєте повернене значення. Я не впевнений, яка найкраща практика, якщо вам дійсно потрібно отримати збережений об’єкт від performSelector - крім "не робити цього".


14

@ З дорогою забезпечує правильний зв'язок з описом проблеми тут . Нижче ви можете побачити мій приклад, коли 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 повертає збережений об'єкт.

Якщо ви запустите інструмент витоку пам’яті, ви побачите наступне зображення: введіть тут опис зображення ... і в будь-якому іншому випадку немає витоків пам’яті: введіть тут опис зображення


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

Небезпечні маршрути

Потрібен якийсь замовчувач компілятора, який неминуче перерветься. Зауважимо, що в даний час вона справді зламалася у Свіфті .

// 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
Каталін

4

Оскільки ви використовуєте ARC, ви повинні використовувати iOS 4.0 або новішу версію. Це означає, що ви можете використовувати блоки. Якщо замість того, щоб запам'ятати селектор, який виконує вас, замість цього взяв блок, ARC зможе краще відстежувати, що відбувається насправді, і вам не доведеться ризикувати випадковим введенням витоку пам'яті.


Насправді, блоки дуже легко випадково створити цикл збереження, який ARC не вирішує. Я все ще хочу, щоб було попередження компілятора, коли ви неявно використовували selfivar (наприклад, 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

У вашому класі, якому потрібно зателефонувати своєму селектору, у вас є властивість @.

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

Коли вам потрібно зателефонувати @selector(doSomethingWithObject:)в екземпляр MyObject, зробіть це:

[self.source doSomethingWithObject:object];

2
Привіт, дякую, але сенс використання NSSelectorFromString полягає в тому, що ви не знаєте, який селектор ви хочете зателефонувати під час виконання.
Eduardo Scoz
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.