Я думаю, что это действительно отличный вопрос, и обидно, что вместо того, чтобы заняться реальным вопросом, большинство ответов обходят проблему и просто говорят, что не следует использовать мошенничество.
Использование метода шипения похоже на использование острых ножей на кухне. Некоторые люди боятся острых ножей, потому что думают, что сильно порежутся, но правда в том, что острые ножи безопаснее .
Метод Swizzling может быть использован для написания лучшего, более эффективного, более удобного в обслуживании кода. Им также можно злоупотреблять и приводить к ужасным ошибкам.
Задний план
Как и во всех шаблонах проектирования, если мы полностью осознаем последствия этого шаблона, мы можем принимать более обоснованные решения о том, использовать его или нет. Одиночки хороший пример того, что это довольно спорный, и не зря - они на самом деле трудно правильно реализовать. Однако многие люди все еще предпочитают использовать синглтоны. То же самое можно сказать и о метании. Вы должны составить собственное мнение, как только вы полностью поймете как хорошее, так и плохое.
обсуждение
Вот некоторые из ловушек метода swizzling:
- Метод метания не атомарный
- Изменяет поведение чужого кода
- Возможные конфликты имен
- Swizzling меняет аргументы метода
- Порядок алкогольных напитков имеет значение
- Трудно понять (выглядит рекурсивно)
- Сложно отлаживать
Все эти пункты верны, и при их рассмотрении мы можем улучшить как наше понимание метода метания, так и методологию, используемую для достижения результата. Я возьму каждый по одному.
Метод метания не атомарный
Мне еще предстоит увидеть реализацию метода Swizzling, который безопасно использовать одновременно 1 . На самом деле это не проблема в 95% случаев, когда вы хотите использовать метод Swizzling. Обычно вы просто хотите заменить реализацию метода, и вы хотите, чтобы эта реализация использовалась в течение всего времени жизни вашей программы. Это означает, что вы должны сделать свой метод вхолостую +(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 (поскольку он использует указатели функций), он избегает любых конфликтов имен. В принципе, он делает то же самое, что и стандартное пьянство. Это может быть небольшим изменением для людей, которые использовали Swizzling, как это было определено некоторое время, но в конце я думаю, что это лучше. Метод Swizzling определяется следующим образом:
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
Swizzling путем переименования методов изменяет аргументы метода
Это большой в моей голове. Это причина того, что стандартный метод Swizzling не должно быть сделано. Вы изменяете аргументы, переданные в реализацию исходного метода. Вот где это происходит:
[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:
кнопку, она вызывает ваш метод swizzled, а затем сразу переходит к 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:)];
Поскольку вид swizzling происходит первый, контроль swizzling сможет подтянуть правильный метод. Кроме того, поскольку управление swizzling было до кнопки swizzling, кнопка будет тянуть swizzled реализацию элемента управления из setFrame:
. Это немного сбивает с толку, но это правильный порядок. Как мы можем обеспечить этот порядок вещей?
Опять же, просто используйте, load
чтобы взбалтывать вещи. Если вы подключитесь load
и внесете изменения только в загружаемый класс, вы будете в безопасности. В load
методе гарантирует , что метод супер нагрузки класса будет вызываться до подклассов. Мы получим точный правильный заказ!
Трудно понять (выглядит рекурсивно)
Глядя на традиционно определяемый метод, я думаю, что действительно трудно сказать, что происходит. Но, глядя на альтернативный способ, которым мы проделали пьянство выше, это довольно легко понять. Это уже решено!
Сложно отлаживать
Одно из недоразумений во время отладки заключается в том, что вы видите странный след, в котором смешанные имена перепутаны и все перемешано в вашей голове. Опять же, альтернативная реализация решает эту проблему. Вы увидите четко названные функции в следах. Тем не менее, отравление может быть трудным для отладки, потому что трудно вспомнить, какое влияние оказывает воспаление. Хорошо документируйте свой код (даже если вы думаете, что вы единственный, кто когда-либо его увидит). Следуйте хорошей практике, и все будет в порядке. Отладка не сложнее, чем многопоточный код.
Вывод
Метод Swizzling безопасен при правильном использовании. Простая мера безопасности, которую вы можете предпринять, состоит в том, чтобы только влететь load
. Как и многие вещи в программировании, это может быть опасно, но понимание последствий позволит вам использовать его правильно.
1 Используя описанный выше метод Swizzling, вы можете сделать вещи безопасными, если вы будете использовать батуты. Вам понадобятся два батута. В начале метода вам нужно будет присвоить указатель store
функции функции, которая вращается до тех пор, пока адрес, на который store
указывает указатель, не изменится. Это позволит избежать любого состояния гонки, при котором метод swizzled вызывался до того, как вы смогли установить store
указатель на функцию. Затем вам нужно будет использовать батут в случае, когда реализация еще не определена в классе, и вам нужно найти батут и правильно вызвать метод суперкласса. Определяя метод так, чтобы он динамически искал, супер реализация гарантирует, что порядок быстрых вызовов не имеет значения.