Рішення
Компілятор попереджає про це з причини. Дуже рідко таке попередження потрібно просто ігнорувати, і його легко обійти. Ось як:
if (!_controller) { return; }
SEL selector = NSSelectorFromString(@"someMethod");
IMP imp = [_controller methodForSelector:selector];
void (*func)(id, SEL) = (void *)imp;
func(_controller, selector);
Або ще більш лаконічно (хоч і важко читати та без охорони):
SEL selector = NSSelectorFromString(@"someMethod");
((void (*)(id, SEL))[_controller methodForSelector:selector])(_controller, selector);
Пояснення
Тут відбувається запитання контролера на покажчик функції C для методу, відповідного контролеру. Усі NSObject
реагують на methodForSelector:
, але ви також можете використовувати class_getMethodImplementation
в режимі виконання Objective-C (корисно, якщо у вас є лише посилання на протокол, наприклад id<SomeProto>
). Ці функціональні вказівники називаються IMP
s, і це прості 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
- Ігноруйте типи
void
, що не є об'єктами ( int
, тощо)
- Збережіть об'єктне значення, а потім відпустіть, коли він більше не використовується (стандартне припущення)
- Вивільнюйте нові значення об'єкта, коли їх більше не застосовується (методи в
init
/ copy
сімействі або приписуються ns_returns_retained
)
- Не робити нічого і не припускати, що повернене значення об'єкта буде дійсним у локальній області (поки внутрішній пул найвищого випуску не буде вичерпано, приписується
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.
З часом ми спостерігали такий прогрес:
- Ранні версії Objective-C дозволяють
performSelector:
(ручне управління пам'яттю)
- Objective-C з ARC попереджає про використання
performSelector:
- Swift не має доступу до
performSelector:
цих методів і документує їх як " суттєво небезпечні"
Однак ідея надсилання повідомлень на основі названого селектора не є особливою небезпекою. Ця ідея давно успішно використовується в Objective-C, а також багатьох інших мовах програмування.
1 Всі методи Objective-C мають дві приховані аргументи, self
і _cmd
які неявно додаються при виклику методу.
2 Виклик NULL
функції не є безпечним у C. Охоронець, який використовується для перевірки наявності контролера, гарантує наявність у нас об'єкта. Тому ми знаємо, що отримаємо IMP
від methodForSelector:
(хоча це може бути _objc_msgForward
і вхід у систему переадресації повідомлень). В основному, при охороні на місці, ми знаємо, що у нас є функція для виклику.
3 Насправді, це може отримати неправильну інформацію, якщо оголосити вас об’єктами як id
і ви не імпортуєте всі заголовки. У вас може виникнути збої в коді, які компілятор вважає нормальним. Це дуже рідко, але може статися. Зазвичай ви отримуєте лише попередження про те, що не знає, який із двох методів підписи вибрати.
4 Див. Посилання ARC щодо збережених значень повернення та незарезервованих значень повернення для отримання детальної інформації.