Ответ 1
Я думаю, что это действительно большой вопрос, и стыдно, что вместо того, чтобы решать реальный вопрос, большинство ответов обошли эту проблему и просто сказали, что не использовать swizzling.
Использование метода sizzling - это как использование острых ножей на кухне. Некоторые люди боятся острых ножей, потому что считают, что они сильно порежутся, но правда в том, что острые ножи более безопасны.
Метод swizzling может использоваться для написания лучшего, более эффективного и удобного кода. Его также можно злоупотреблять и приводить к ужасным ошибкам.
Фон
Как и во всех шаблонах проектирования, если мы полностью осознаем последствия шаблона, мы можем принимать более обоснованные решения о том, использовать или не использовать его. Одиночки хороший пример того, что довольно спорный, и по уважительной причине и— их действительно сложно реализовать должным образом. Тем не менее, многие люди все же предпочитают использовать одиночные игры. То же самое можно сказать о swizzling. Вы должны сформировать свое собственное мнение, как только вы полностью поймете как хорошее, так и плохое.
Обсуждение
Вот некоторые из ловушек метода swizzling:
- Метод swizzling не является атомарным
- Изменяет поведение не принадлежащего ему кода
- Возможные конфликты имен
- Swizzling изменяет аргументы метода
- Порядок упражнений
- Трудно понять (выглядит рекурсивным)
- Сложно отлаживать
Все эти точки действительны, и при их решении мы можем улучшить как наше понимание метода swizzling, так и методологию, используемую для достижения результата. Я возьму каждого за раз.
Метод swizzling не является атомарным
Мне еще предстоит увидеть реализацию метода swizzling, которая безопасна для использования одновременно 1. На самом деле это не проблема в 95% случаев, когда вы хотите использовать метод swizzling. Обычно вы просто хотите заменить реализацию метода, и хотите, чтобы эта реализация использовалась на протяжении всего срока службы вашей программы. Это означает, что вы должны выполнить свой метод swizzling в +(void)load
. Метод класса load
выполняется последовательно в начале вашего приложения. У вас не будет проблем с concurrency, если вы будете делать это. Однако, если вы должны были подкачать в +(void)initialize
, вы можете закончить условие гонки в своей swizzling-реализации, и время выполнения может оказаться в странном состоянии.
Изменяет поведение не принадлежащего ему кода
Это проблема с swizzling, но это как бы весь смысл. Цель состоит в том, чтобы иметь возможность изменять этот код. Причина, по которой люди считают это большой проблемой, состоит в том, что вы не просто меняете вещи для одного экземпляра NSButton
, для которого вы хотите изменить вещи, а вместо этого для всех экземпляров NSButton
в вашем приложении. По этой причине вы должны быть осторожны, когда вы swizzle, но вам не нужно вообще избегать этого.
Подумайте об этом так: если вы переопределите метод в классе и вы не вызываете метод суперкласса, вы можете вызвать проблемы. В большинстве случаев суперкласс ожидает, что этот метод будет вызван (если не указано иначе). Если вы примените эту же мысль к swizzling, вы рассмотрели большинство проблем. Всегда вызывайте первоначальную реализацию. Если вы этого не сделаете, вы, вероятно, измените слишком много, чтобы быть в безопасности.
Возможные конфликты имен
Именование конфликтов является проблемой на всем протяжении Cocoa. Мы часто префикс имен классов и имен методов в категориях. К сожалению, конфликты имен - это чума на нашем языке. Однако в случае с swizzling они не обязательно должны быть. Нам просто нужно изменить способ, который мы думаем о методе swizzling слегка. Большинство swizzling выполняется следующим образом:
@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:
было определено где-то еще? Эта проблема не уникальна для swizzling, но мы все равно можем ее обойти. Обходной путь имеет дополнительное преимущество для решения других проблем. Вот что мы делаем вместо этого:
@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, поскольку это было определено некоторое время, но, в конце концов, я думаю, что это лучше. Метод 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:
. Первоначальная реализация вызывается с аргументом, который он никогда не ожидал получить. Это нехорошо.
Там простое решение — используйте альтернативную методику, описанную выше. Аргументы остаются неизменными!
Порядок действий swizzles
Порядок, в котором методы подвергаются проверке. Предполагая, что 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
. То же самое произойдет при swizzling на NSControl
(снова используя реализацию NSView
).
Когда вы вызываете setFrame:
на кнопку, это вызовет ваш swizzled метод, а затем перейдет прямо к методу setFrame:
, первоначально определенному на NSView
. Запускаемые версии NSControl
и NSView
не будут вызываться.
Но что, если порядок был:
[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
гарантирует, что метод загрузки суперкласса будет вызываться перед любыми подклассами. Мы получим правильный порядок!
Трудно понять (выглядит рекурсивным)
Глядя на традиционно определенный swizzled метод, я думаю, что действительно сложно сказать, что происходит. Но, глядя на альтернативный способ, который мы делали над собой, это довольно легко понять. Это уже было решено!
Сложно отлаживать
Одно из путаниц во время отладки - это странная обратная трассировка, где запутанные имена перепутаны, и все становится беспорядочным в вашей голове. Опять же, альтернативная реализация решает эту проблему. Вы увидите четко названные функции в backtraces. Тем не менее, swizzling может быть трудно отлаживать, потому что трудно вспомнить, какое влияние оказывает swizzling. Хорошо документируйте свой код (даже если вы думаете, что вы единственный, кто когда-либо его увидит). Следуйте за хорошими практиками, и с вами все будет в порядке. Это не сложно отлаживать, чем многопоточный код.
Заключение
Метод swizzling безопасен при правильном использовании. Простая мера безопасности, которую вы можете предпринять, состоит только в том, чтобы зависеть только в load
. Как и многое в программировании, это может быть опасно, но понимание последствий позволит вам правильно использовать его.
1 Используя вышеописанный метод swizzling, вы можете сделать вещи потокобезопасными, если вы будете использовать батуты. Вам понадобится два батута. В начале метода вам нужно назначить указатель функции store
функции, которая вращается, пока не будет указан адрес, на который указывает store
. Это позволит избежать любого состояния гонки, в котором был вызван swizzled метод, прежде чем вы смогли установить указатель функции store
. Затем вам нужно будет использовать батут в случае, когда реализация еще не определена в классе и имеет поиск в батуте, и правильно вызовите метод суперкласса. Определяя метод, чтобы он динамически просматривал супер-реализацию, будет гарантировать, что порядок вызовов с помощью swizzling не имеет значения.