Які небезпеки шиплячих методів у Objective-C?


235

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

Метод Swizzling - це модифікація відображення таким чином, що виклик селектора A насправді викликатиме реалізацію B. Одним із застосувань цього є розширення поведінки закритих джерельних класів.

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

Напр

  • Іменування зіткнень : Якщо клас пізніше розширить свою функціональність, щоб включити ім'я методу, який ви додали, це спричинить величезну кількість проблем. Зменшіть ризик, розумно називаючи шиплячі методи.

Це вже не дуже добре отримати щось інше, чого ви очікуєте від прочитаного вами коду
Мартін Бабакаєв

це звучить як нечистий спосіб робити те, що декоратори пітона роблять дуже чисто
Клавдіу,

Відповіді:


439

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

Використовувати метод шиплячого - це як використовувати гострі ножі на кухні. Деякі люди лякаються гострих ножів, бо думають, що поріжуть себе погано, але правда - гострі ножі безпечніші .

Метод завитка може бути використаний для написання кращого, більш ефективного та більш доступного коду. Це також може зловживати і призводити до жахливих помилок.

Фон

Як і у всіх моделях дизайну, якщо ми повністю усвідомлюємо наслідки цього шаблону, ми можемо приймати більш обґрунтовані рішення щодо того, використовувати чи ні. Сінглтони - хороший приклад того, що є досить суперечливим, і з поважної причини - їх справді важко реалізувати належним чином. Однак багато людей все ж вирішили використовувати одиночні кнопки. Те саме можна сказати і про шипіння. Ви повинні сформувати власну думку, коли ви повністю зрозумієте і хороше, і погане.

Обговорення

Ось декілька підводних помилок методу:

  • Метод свистування не атомний
  • Змінює поведінку невласного коду
  • Можливі імена конфліктів
  • Swizzling змінює аргументи методу
  • Порядок суєт має значення
  • Важко зрозуміти (виглядає рекурсивно)
  • Складно налагоджувати

Усі ці пункти справедливі, і, вирішуючи їх, ми можемо покращити як наше розуміння методу шиплячих, так і методологію, що використовується для досягнення результату. Я візьму кожного по одному.

Метод свистування не атомний

Мені ще не доводиться бачити реалізацію методу шиплячого, який безпечно використовувати одночасно 1 . Це насправді не є проблемою у 95% випадків, коли ви хочете скористатися методом шиплячого. Зазвичай ви просто хочете замінити реалізацію методу, і ви хочете, щоб ця реалізація була використана протягом усього життя вашої програми. Це означає, що ви повинні виконувати свій метод шипучі +(void)load. Метод loadкласу виконується послідовно на початку вашої програми. У вас не виникне жодних проблем із одночасністю, якщо ви зробите тут шипіння. Якщо ви повинні промайструвати +(void)initialize, проте, ви можете перетворитись на стан гонки у вашому закрученому виконанні, і час виконання може закінчитися у дивному стані.

Змінює поведінку невласного коду

Це питання з хиткою, але це все-таки суть. Мета полягає в тому, щоб мати можливість змінити цей код. Причина того, що люди вказують на це як на велику справу, полягає в тому, що ви не просто змінюєте речі для одного екземпляра, на NSButtonякий хочете змінити, а натомість для всіх NSButtonвипадків вашої програми. З цієї причини вам слід бути обережними, коли ви плітаєте, але не потрібно уникати цього взагалі.

Подумайте про це таким чином ... якщо ви перекриєте метод у класі і не викликаєте метод суперкласу, це може спричинити проблеми. У більшості випадків суперклас очікує виклику цього методу (якщо документально не підтверджено інше). Якщо ви застосуєте цю саму думку до шиплячих, ви охопили більшість проблем. Завжди закликайте оригінальну реалізацію. Якщо ви цього не зробите, ви, ймовірно, змінюєтеся занадто багато, щоб бути безпечним.

Можливі імена конфліктів

Називання конфліктів є проблемою у всьому Какао. Ми часто префіксуємо назви класів та назви методів у категоріях. На жаль, імена конфліктів - це чума на нашій мові. У випадку, коли хитаються, вони не повинні бути. Нам просто потрібно змінити спосіб, який ми думаємо про метод, який незначно шипить. Більшість завитків робиться так:

@interface NSView : NSObject
- (void)setFrame:(NSRect)frame;
@end

@implementation NSView (MyViewAdditions)

- (void)my_setFrame:(NSRect)frame {
    // do custom work
    [self my_setFrame:frame];
}

+ (void)load {
    [self swizzle:@selector(setFrame:) with:@selector(my_setFrame:)];
}

@end

Це працює просто чудово, але що буде, якби my_setFrame:було визначено десь інше? Ця проблема не властива лише шиплячим, але ми можемо її вирішити в будь-якому випадку. Вирішення проблеми має додаткову користь у вирішенні інших підводних каменів. Ось що ми робимо замість цього:

@implementation NSView (MyViewAdditions)

static void MySetFrame(id self, SEL _cmd, NSRect frame);
static void (*SetFrameIMP)(id self, SEL _cmd, NSRect frame);

static void MySetFrame(id self, SEL _cmd, NSRect frame) {
    // do custom work
    SetFrameIMP(self, _cmd, frame);
}

+ (void)load {
    [self swizzle:@selector(setFrame:) with:(IMP)MySetFrame store:(IMP *)&SetFrameIMP];
}

@end

Хоча це виглядає трохи менше як Objective-C (оскільки він використовує функціональні вказівники), це дозволяє уникнути будь-яких конфліктів імен. В принципі, це робиться точно так само, як і стандартне шипіння. Це може трохи змінити людей, які користуються шипшиною, як це було визначено певний час, але врешті-решт, я думаю, що це краще. Метод шипування визначається таким чином:

typedef IMP *IMPPointer;

BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) {
    IMP imp = NULL;
    Method method = class_getInstanceMethod(class, original);
    if (method) {
        const char *type = method_getTypeEncoding(method);
        imp = class_replaceMethod(class, original, replacement, type);
        if (!imp) {
            imp = method_getImplementation(method);
        }
    }
    if (imp && store) { *store = imp; }
    return (imp != NULL);
}

@implementation NSObject (FRRuntimeAdditions)
+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {
    return class_swizzleMethodAndStore(self, original, replacement, store);
}
@end

Швидкість перейменування методів змінює аргументи методу

Це найбільше на моєму розумі. Це є причиною того, що не слід робити стандартних методів. Ви змінюєте аргументи, передані для реалізації оригінального методу. Ось де це відбувається:

[self my_setFrame:frame];

Цей рядок:

objc_msgSend(self, @selector(my_setFrame:), frame);

Що використовуватиме час виконання для пошуку виконання my_setFrame:. Як тільки реалізація знайдена, вона викликає реалізацію тими ж аргументами, що й наведені. Знайдена ним реалізація є оригінальною реалізацією setFrame:, тому вона продовжує і вимагає цього, але _cmdаргумент не setFrame:такий, як повинен бути. Це зараз my_setFrame:. Початкова реалізація викликається аргументом, який ніколи не очікував, що отримає. Це не добре.

Є просте рішення - використовуйте альтернативну техніку шипування, визначену вище. Аргументи залишаться незмінними!

Порядок суєт має значення

Порядок, в якому способи отримують невідомі значення, має значення. Припускаючи setFrame:, що визначено лише NSView, уявіть собі такий порядок речей:

[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];

Що відбувається, коли метод увімкнено NSButton? Ну більшість swizzling гарантуватиме , що він не замінює реалізацію setFrame:для всіх уявлень, тому він буде тягнути метод примірника. Це дозволить використовувати існуючу реалізацію для повторного визначення setFrame:в NSButtonкласі, щоб обмін реалізаціями не впливав на всі представлення даних. Існуюча реалізація визначена на NSView. Те ж саме станеться і при включенні NSControl(знову використання NSViewреалізації).

Коли ви setFrame:натискаєте кнопку, то вона зателефонує вашому зашифрованому методу, а потім перейде прямо до setFrame:методу, спочатку визначеного на NSView. В NSControlі NSViewswizzled реалізації не буде називатися.

Але що робити, якщо наказ:

[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];

Оскільки запаморочення огляду відбувається першим, контрольне шиття зможе підтягнути правильний метод. Аналогічно, оскільки шинання керування відбулося до того, як кнопка закружляла, кнопка підтягуватиме реалізацію управління setFrame:. Це трохи заплутано, але це правильний порядок. Як ми можемо забезпечити такий порядок речей?

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

Важко зрозуміти (виглядає рекурсивно)

Дивлячись на традиційно визначений химерний метод, я думаю, що насправді важко сказати, що відбувається. Але дивлячись на альтернативний спосіб, який ми зробили шиплячим вище, це зрозуміти досить просто. Це вже вирішено!

Складно налагоджувати

Одне з плутанини під час налагодження - бачити дивний зворотній шлях, коли заплутані імена змішуються, і все заскакує у вас в голові. Знову ж, альтернативна реалізація вирішує це. Ви побачите чітко названі функції в backtraces. Тим не менш, скрут може бути важким для налагодження, тому що важко запам’ятати, який вплив має шипшина. Задокументуйте свій код добре (навіть якщо ви думаєте, що ви єдиний, хто коли-небудь його побачить). Дотримуйтесь належних практик, і ви все в порядку. Налагоджувати не важче, ніж багатопотоковий код.

Висновок

Метод шипування безпечний при правильному використанні. Простий запобіжний захід, який ви можете вжити, - це лише тріскатися load. Як і багато речей у програмуванні, це може бути небезпечно, але розуміння наслідків дозволить правильно використовувати його.


1 Використовуючи вищеописаний метод шипування, ви могли б зробити речі нитки безпечними, якби використовували батути. Вам знадобиться два батути. На початку методу вам доведеться призначити покажчик функції store, - функції, яка крутиться до тих пір, поки storeне зміниться адреса, на яку вказано. Це дозволить уникнути будь-яких перегонових умов, в яких закликався метод swizzled, перш ніж ви змогли встановити storeпокажчик функції. Тоді вам потрібно буде використовувати батут у тому випадку, коли реалізація ще не визначена в класі, а пошук батута і належним чином викликати метод суперкласу. Визначивши метод, щоб він динамічно шукав, супер реалізація забезпечить, щоб порядок шипучих викликів не має значення.


24
Дивовижна інформативна та технічно обгрунтована відповідь. Дискусія справді цікава. Дякуємо, що знайшли час, щоб написати це.
Рікардо Санчес-Саес

Не могли б ви зазначити, чи запаморочення у вашому досвіді є прийнятним у магазинах додатків. Я направляю вас до stackoverflow.com/questions/8834294 .
Дікі Сінгх

3
Swizzling можна використовувати в магазині додатків. Багато додатків і фреймворків (у нас включено). Все вище все ще має місце, і ви не можете переплутати приватні методи (насправді ви технічно можете, але ви ризикуєте відхилитись, і небезпеки цього полягають в іншому потоці).
wbyoung

Дивовижна відповідь. Але навіщо робити перекручування в class_swizzleMethodAndStore () замість прямо в + swizzle: з: store:? Чому саме ця додаткова функція?
Фрізлаб

2
@Frizlab гарне запитання! Це справді просто річ у стилі. Якщо ви пишете купу коду, який працює безпосередньо з API виконання Objective-C (в С), то приємно мати можливість називати його стилем C за послідовністю. Єдина причина, про яку я можу подумати, окрім цього, це якщо ви щось написали на чистому мові C, то це все ще можна дзвонити. Однак ви не можете зробити це все методом Objective-C.
wbyoung

12

Спочатку я точно визначу, що я маю на увазі під методом шиплячих:

  • Перенаправлення всіх викликів, які спочатку були надіслані методом (званий А), до нового методу (званого B).
  • У нас є метод B
  • Ми не володіємо власним методом A
  • Метод B виконує деяку роботу, потім викликає метод А.

Метод свистування більш загальний, ніж цей, але мене це цікавить той випадок.

Небезпеки:

  • Зміни в оригінальному класі . Ми не володіємо класом, який ми кружляємо. Якщо клас зміниться, наша свистка може перестати працювати.

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

  • Важко налагодити . Важко стежити за течією снігу, деякі люди можуть навіть не здогадуватися про те, що виймка була виконана. Якщо є помилки, введені в swizzle (можливо, внесення змін до початкового класу), їх буде важко вирішити.

Підводячи підсумок, ви повинні звести свіжіння до мінімуму і подумати, як зміни в оригінальному класі можуть вплинути на ваш шум. Також слід чітко коментувати і документувати те, що ви робите (або просто уникати цього цілком).


@everyone Я поєднав ваші відповіді в хороший формат. Не соромтесь редагувати / додавати до нього. Дякуємо за ваш внесок
Роберт

Я шанувальник вашої психології. "З великою силою запаморочення настає велика шипляча відповідальність".
Джексонкр

7

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


Я б сказав, що такі розробники повинні пожинати те, що вони сіють - вони щільно прив'язали свій код до певної реалізації. Тому їхня вина, якщо та реалізація змінюється тонко, що не повинно було порушити їх код, якби не було їх явного рішення з'єднати код так щільно, як це було.
Арафангіон

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

Apple - не єдиний постачальник базових класів, який можна модифікувати за допомогою siwzzling. Ваша відповідь повинна бути відредагована, щоб включити всіх.
AR

@AR Можливо, але питання позначено iOS та iPhone, тому ми повинні розглядати це в цьому контексті. Зважаючи на те, що програми iOS повинні статично зв’язувати будь-які бібліотеки та рамки, окрім тих, що надаються iOS, Apple насправді є єдиним об'єктом, який може змінити реалізацію рамкових класів без участі розробників додатків. Але я погоджуюсь з тим, що подібні питання стосуються інших платформ, і я б сказав, що поради можна також узагальнити і поза шиплячим. Загалом, порада така: Тримайте руки до себе. Уникайте модифікації компонентів, які підтримують інші люди.
Калеб

Метод свистування може бути таким небезпечним. але коли рамки виходять, вони повністю не опускають попередні. вам все одно доведеться оновити базову основу, яку очікує ваш додаток. і це оновлення порушить ваш додаток. це, ймовірно, не буде настільки великою проблемою, коли ios буде оновлено. Моя схема використання полягала в тому, щоб замінити типовий повторний идентификатор повторного використання. Оскільки я не мав доступу до змінної _reuseIdentifier і не хотів її замінювати, якщо вона не була надана, моє єдине рішення було підкласифікувати кожен і надати ідентифікатор повторного використання, або переключити та замінити, якщо нуль. Тип реалізації є ключовим ...
Ледачий кодер

5

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

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

Наприклад, одним із добрих застосувань методу шиплячого є isiz shizling, саме так ObjC реалізує спостереження ключових значень.

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


1
-1 на згадку про KVO - в контексті методу шипування . Іза завірюха - це просто щось інше.
Микола Рухе

@NikolaiRuhe: Будь ласка, не соромтеся надавати посилання, щоб я міг бути освіченим.
Арафангіон

Ось посилання на деталі щодо впровадження KVO . Метод шипування описаний на CocoaDev.
Микола Рухе

5

Хоча я використовував цю методику, я хотів би зазначити, що:

  • Це обтяжує ваш код, оскільки може викликати недокументовані, хоча і бажані, побічні ефекти. Коли хтось читає код, він / вона може не знати про поведінку побічних ефектів, якщо він / вона не пам’ятає шукати базу коду, щоб побачити, чи не був зафіксований. Я не впевнений, як полегшити цю проблему, оскільки не завжди вдається документувати кожне місце, де код залежить від звичного поведінки побічних ефектів.
  • Це може зробити ваш код менш корисним для повторного використання, тому що той, хто знайде сегмент коду, який залежить від поведінки, що їх захотіли використати в іншому місці, не може просто вирізати та вставити його в якусь іншу базу коду, не знайшовши і не скопіювавши закручений метод.

4

Я відчуваю, що найбільша небезпека полягає у створенні багатьох небажаних побічних ефектів, абсолютно випадково. Ці побічні ефекти можуть представити себе як «помилки», що, в свою чергу, призводить вас до неправильного шляху пошуку рішення. На мій досвід, небезпека - нерозбірливий, заплутаний і неприємний код. Начебто, коли хтось надто зловживає функціональними покажчиками в C ++.


Гаразд, проблема полягає в тому, що важко налагоджувати. Проблеми викликані тим, що рецензенти не усвідомлюють / не забувають, що код був заблокований та шукають у неправильному місці під час налагодження.
Роберт

3
Так, дуже багато. Крім того, при надмірному використанні ви можете потрапити в нескінченний ланцюжок або павутинну мережу. Уявіть, що може статися, коли ви зміните лише один із них у якийсь момент, це майбутнє? Я порівнюю цей досвід із складанням чашок або грою в «code jenga»
AR

4

У вас може виникнути начебто дивний код

- (void)addSubview:(UIView *)view atIndex:(NSInteger)index {
    //this looks like an infinite loop but we're swizzling so default will get called
    [self addSubview:view atIndex:index];

від фактичного виробничого коду, пов'язаного з деякою магією інтерфейсу користувача.


3

Метод свистування може бути дуже корисним в одиничному тестуванні.

Це дозволяє записати макетний об’єкт і використовувати цей макетний об’єкт замість реального об'єкта. Ваш код повинен залишатися чистим, а ваш тест на пристрій має передбачувану поведінку. Скажімо, ви хочете перевірити деякий код, який використовує CLLocationManager. Ваш тест одиниці може зміцнити startUpdatingLocation, щоб він подав делегату заздалегідь визначений набір місць, і ваш код не міг би змінюватися.


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