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

235

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

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

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

Например

  • Столкновения именования : если впоследствии класс расширит свою функциональность, добавив имя метода, которое вы добавили, это вызовет множество проблем. Уменьшите риск, разумно назвав изжогенные методы.
Роберт
источник
Уже не очень хорошо получить что-то еще, что вы ожидаете от кода, который вы прочитали
Мартин Бабакаев
это звучит как нечистый способ делать то, что декораторы python делают очень чисто
Claudiu

Ответы:

439

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

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

Метод 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и 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:)];

Поскольку вид swizzling происходит первый, контроль swizzling сможет подтянуть правильный метод. Кроме того, поскольку управление swizzling было до кнопки swizzling, кнопка будет тянуть swizzled реализацию элемента управления из setFrame:. Это немного сбивает с толку, но это правильный порядок. Как мы можем обеспечить этот порядок вещей?

Опять же, просто используйте, loadчтобы взбалтывать вещи. Если вы подключитесь loadи внесете изменения только в загружаемый класс, вы будете в безопасности. В loadметоде гарантирует , что метод супер нагрузки класса будет вызываться до подклассов. Мы получим точный правильный заказ!

Трудно понять (выглядит рекурсивно)

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

Сложно отлаживать

Одно из недоразумений во время отладки заключается в том, что вы видите странный след, в котором смешанные имена перепутаны и все перемешано в вашей голове. Опять же, альтернативная реализация решает эту проблему. Вы увидите четко названные функции в следах. Тем не менее, отравление может быть трудным для отладки, потому что трудно вспомнить, какое влияние оказывает воспаление. Хорошо документируйте свой код (даже если вы думаете, что вы единственный, кто когда-либо его увидит). Следуйте хорошей практике, и все будет в порядке. Отладка не сложнее, чем многопоточный код.

Вывод

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


1 Используя описанный выше метод Swizzling, вы можете сделать вещи безопасными, если вы будете использовать батуты. Вам понадобятся два батута. В начале метода вам нужно будет присвоить указатель storeфункции функции, которая вращается до тех пор, пока адрес, на который storeуказывает указатель, не изменится. Это позволит избежать любого состояния гонки, при котором метод swizzled вызывался до того, как вы смогли установить storeуказатель на функцию. Затем вам нужно будет использовать батут в случае, когда реализация еще не определена в классе, и вам нужно найти батут и правильно вызвать метод суперкласса. Определяя метод так, чтобы он динамически искал, супер реализация гарантирует, что порядок быстрых вызовов не имеет значения.

wbyoung
источник
24
Удивительно информативный и технически обоснованный ответ. Дискуссия действительно интересная. Спасибо, что нашли время, чтобы написать это.
Рикардо Санчес-Саез
Не могли бы вы указать, приемлемо ли в ваших магазинах приложение к игре? Я также направляю вас на stackoverflow.com/questions/8834294 .
Дики Сингх
3
Swizzling можно использовать в магазине приложений. Многие приложения и платформы (включая наши). Все вышеперечисленное остается в силе, и вы не можете использовать частные методы (на самом деле, вы можете это сделать технически, но вы рискуете быть отвергнутыми, и опасность этого для другого потока).
13
Удивительный ответ. Но зачем делать swizzling в class_swizzleMethodAndStore (), а не прямо в + swizzle: with: store :? Почему эта дополнительная функция?
Frizlab
2
@Frizlab хороший вопрос! Это действительно просто вещь стиля. Если вы пишете кучу кода, который работает напрямую с API-интерфейсом Objective-C (в C), то было бы хорошо назвать его стилем C для согласованности. Единственная причина, о которой я могу подумать, кроме того, что если вы написали что-то на чистом C, то это еще более вызываемо. Однако нет причины, по которой вы не можете делать все это в методе Objective-C.
wbyoung
12

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

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

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

Опасности:

  • Изменения в оригинальном классе . Нам не принадлежит класс, который мы извергаем. Если класс изменится, наш алкогольный напиток может перестать работать.

  • Трудно поддерживать . Мало того, что вы должны написать и поддерживать метод Swizzled. Вы должны написать и поддерживать код, который преобразует Swizzle

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

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

Роберт
источник
@ Все, я объединил ваши ответы в хороший формат. Не стесняйтесь редактировать / добавлять к нему. Спасибо за ваш вклад.
Роберт
Я фанат вашей психологии. «С огромной бурной силой приходит великая бурная ответственность».
Джексонкр
7

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

Калеб
источник
Я бы сказал, что такие разработчики должны пожинать плоды того, что они сеют - они тесно связывают свой код с конкретной реализацией. Поэтому они виноваты в том, что эта реализация изменяется тонким способом, который не должен был нарушать их код, если бы не было их явного решения соединить их код так же тесно, как они.
Арафангион
Конечно, но говорят, что очень популярное приложение ломается под следующей версией iOS. Легко сказать, что это ошибка разработчика, и они должны знать лучше, но это заставляет Apple выглядеть плохо в глазах пользователя. Я не вижу никакого вреда в использовании ваших собственных методов или даже классов, если вы хотите это сделать, кроме того, что вы можете сделать код несколько запутанным. Это начинает становиться немного более рискованным, когда вы перебиваете код, который контролируется кем-то другим - и это именно та ситуация, когда перехлестывание кажется наиболее заманчивым.
Калеб
Apple не единственный поставщик базовых классов, которые могут быть изменены с помощью siwzzling. Ваш ответ должен быть отредактирован, чтобы включить всех.
AR
@AR Возможно, но вопрос помечен iOS и iPhone, поэтому мы должны рассмотреть его в этом контексте. Учитывая, что приложения iOS должны статически связывать любые библиотеки и платформы, кроме тех, которые предоставляются iOS, Apple фактически является единственным объектом, который может изменить реализацию классов инфраструктуры без некоторого участия разработчиков приложений. Но я согласен с тем, что подобные проблемы влияют и на другие платформы, и я бы сказал, что этот совет может быть обобщен и без проблем. В общем виде совет: держите руки при себе. Избегайте изменения компонентов, которые поддерживают другие люди.
Калеб
Метод метания может быть настолько опасным. но по мере появления рамок они не полностью опускают предыдущие. вам все еще нужно обновить базовую среду, которую ожидает ваше приложение. и это обновление сломало бы ваше приложение. Вероятно, это не будет такой большой проблемой при обновлении ios. Мой шаблон использования был переопределить reuseIdentifier по умолчанию. Так как у меня не было доступа к переменной _reuseIdentifier, и я не хотел ее заменять, если она не была предоставлена, мое единственное решение состояло в том, чтобы создать подкласс каждого и предоставить идентификатор повторного использования, либо swizzle и переопределить, если ноль. Тип реализации является ключевым ...
Ленивый кодер
5

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

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

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

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

Arafangion
источник
1
-1 за упоминание КВО - в контексте метода swizzling . Isa Swizzling это просто что-то еще.
Николай Рухе
@NikolaiRuhe: Пожалуйста, не стесняйтесь давать ссылки, чтобы я мог быть просветленным.
Арафангион
Вот ссылка на детали реализации KVO . Метод Swizzling описан на CocoaDev.
Николай Рухе
5

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

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

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

Арканзас
источник
Итак, проблема в том, что это трудно отладить. Проблемы вызваны тем, что рецензенты не понимают / не забывают, что код был переполнен и при отладке ищут неправильное место.
Роберт
3
Да, в значительной степени. Кроме того, при чрезмерном использовании вы можете попасть в бесконечную цепочку или паутину алкогольных напитков. Представьте, что может произойти, если вы поменяете один из них в какой-то момент в будущем? Я сравниваю этот опыт со сложением чайных чашек или игрой в «код дженгу»
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];

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

quantumpotato
источник
3

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

Это позволяет вам писать фиктивный объект и использовать этот фиктивный объект вместо реального объекта. Ваш код остается чистым, а ваш модульный тест - предсказуемым. Допустим, вы хотите протестировать некоторый код, который использует CLLocationManager. Ваш модульный тест может быстро запустить startUpdatingLocation, чтобы он передавал предопределенный набор местоположений вашему делегату, и ваш код не должен был изменяться.

user3288724
источник
Isa-Swizzling будет лучшим выбором.
Викингосегундо