Я думаю, що це справді велике запитання, і прикро, що замість того, щоб вирішувати справжнє запитання, більшість відповідей перекрутили питання і просто сказали, що не використовувати шипучість.
Використовувати метод шиплячого - це як використовувати гострі ножі на кухні. Деякі люди лякаються гострих ножів, бо думають, що поріжуть себе погано, але правда - гострі ножі безпечніші .
Метод завитка може бути використаний для написання кращого, більш ефективного та більш доступного коду. Це також може зловживати і призводити до жахливих помилок.
Фон
Як і у всіх моделях дизайну, якщо ми повністю усвідомлюємо наслідки цього шаблону, ми можемо приймати більш обґрунтовані рішення щодо того, використовувати чи ні. Сінглтони - хороший приклад того, що є досить суперечливим, і з поважної причини - їх справді важко реалізувати належним чином. Однак багато людей все ж вирішили використовувати одиночні кнопки. Те саме можна сказати і про шипіння. Ви повинні сформувати власну думку, коли ви повністю зрозумієте і хороше, і погане.
Обговорення
Ось декілька підводних помилок методу:
- Метод свистування не атомний
- Змінює поведінку невласного коду
- Можливі імена конфліктів
- 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
і NSView
swizzled реалізації не буде називатися.
Але що робити, якщо наказ:
[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
покажчик функції. Тоді вам потрібно буде використовувати батут у тому випадку, коли реалізація ще не визначена в класі, а пошук батута і належним чином викликати метод суперкласу. Визначивши метод, щоб він динамічно шукав, супер реалізація забезпечить, щоб порядок шипучих викликів не має значення.