Каковы опасности метода Swizzling в Objective-C?

Я слышал, что люди утверждают, что метод метания - опасная практика. Даже название, извергающее, предполагает, что это немного обман.

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

Можем ли мы формализовать риски, чтобы каждый, кто решает, использовать ли Swizzling, мог принять осознанное решение, стоит ли оно того, что он пытается сделать.

Например

  • Столкновения именования: если впоследствии класс расширит свою функциональность, добавив имя метода, которое вы добавили, это вызовет множество проблем. Уменьшите риск, разумно назвав извращенные методы.

Ответ 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 не имеет значения.

Ответ 2

Сначала я точно определяю, что я подразумеваю под методом swizzling:

  • Повторная маршрутизация всех вызовов, которые были первоначально отправлены методу (называемому A), на новый метод (называемый B).
  • У нас есть метод B
  • У нас нет метода A
  • Метод B выполняет некоторую работу, затем вызывает метод A.

Метод swizzling более общий, чем этот, но это тот случай, который меня интересует.

Опасности:

  • Изменения в исходном классе. Мы не владеем классом, который мы изучаем. Если класс изменится, наш swizzle может перестать работать.

  • Трудно поддерживать. Не только вы должны писать и поддерживать swizzled метод. вам нужно написать и сохранить код, который задерживает swizzle

  • Жестко отлаживать. Трудно следить за потоком swizzle, некоторые люди даже не могут понять, что swizzle был подготовлен. Если есть ошибки, вызванные swizzle (возможно, сборы для изменений в исходном классе), их будет трудно решить.

В итоге вы должны свести к минимуму swizzling и подумать о том, как изменения в исходном классе могут повлиять на ваш swizzle. Также вы должны четко комментировать и документировать, что вы делаете (или просто избегать его полностью).

Ответ 3

Это не само феерическое, что действительно опасно. Проблема заключается, как вы говорите, в том, что она часто используется для изменения поведения классов инфраструктуры. Предполагая, что вы знаете что-то о том, как эти частные классы работают, это "опасно". Даже если ваши модификации работают сегодня, всегда есть шанс, что Apple изменит класс в будущем и заставит ваши изменения сломаться. Кроме того, если это делают многие другие приложения, это значительно усложняет для Apple изменение структуры без нарушения работы существующего программного обеспечения.

Ответ 4

Используется тщательно и мудро, это может привести к элегантному коду, но обычно это просто приводит к запутыванию кода.

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

Например, одно хорошее применение метода swizzling isa swizzling, так как ObjC реализует наблюдение за ключевым значением.

Неверный пример может полагаться на метод swizzling как средство расширения ваших классов, что приводит к чрезвычайно высокой связи.

Ответ 5

Хотя я использовал эту технику, я хотел бы указать, что:

  • Он запутывает ваш код, потому что он может вызвать незарегистрированные, хотя и желаемые, побочные эффекты. Когда вы читаете код, он/она может не знать о поведении побочных эффектов, которое требуется, если он/она не помнит, чтобы искать базу кода, чтобы проверить, была ли она проверена. Я не уверен, как облегчить эту проблему, потому что не всегда возможно документировать все места, где код зависит от поведения, вызванного побочным эффектом.
  • Это может сделать ваш код менее многоразовым, потому что тот, кто находит сегмент кода, который зависит от swizzled поведения, который они хотели бы использовать в другом месте, не может просто вырезать и вставлять его в какую-либо другую базу кода, не обнаруживая и не копируя swizzled method.

Ответ 6

Я чувствую, что самая большая опасность заключается в создании многих нежелательных побочных эффектов, полностью случайно. Эти побочные эффекты могут представлять собой "ошибки", которые, в свою очередь, приводят вас к неправильному пути поиска решения. По моему опыту, опасность неразборчива, сбивает с толку и расстраивает код. Вид вроде, когда кто-то переоценивает указатели на функции в С++.

Ответ 7

Вы можете получить код нечетного вида, например

- (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];

из фактического производственного кода, связанного с некоторой магией интерфейса.

Ответ 8

Метод swizzling может быть очень полезным при модульном тестировании.

Он позволяет вам писать макет объекта и использовать этот макет объекта вместо реального объекта. Ваш код останется чистым, а ваш unit test имеет предсказуемое поведение. Скажем, вы хотите протестировать код, который использует CLLocationManager. Ваш unit test может swizzle startUpdatingLocation, чтобы он передавал заданный набор мест вашему делегату, и ваш код не изменился бы.