Jakie są zagrożenia płynące ze stosowania metody w Objective C?

Słyszałem, że ludzie twierdzą, że metoda jest niebezpieczną praktyką. Nawet nazwa swizzling sugests, że jest to trochę oszustwo.

Metoda Swizzling modyfikuje mapowanie tak, aby wywołanie selektora a faktycznie wywołało implementację B. jednym z jej zastosowań jest rozszerzenie zachowania klas closed source.

Czy możemy sformalizować ryzyko tak, aby każdy, kto decyduje, czy skorzystać z swizzling, mógł podjąć świadomą decyzję, czy warto za to, co próbują to zrobić.

Np.

  • nazewnictwo kolizji: jeśli Klasa później rozszerzy swoją funkcjonalność o nazwę metody, którą dodałeś, spowoduje to ogromne problemy. Zmniejsz ryzyko, rozsądnie nazywając metody swizzled.
Author: James Webster, 2011-03-17

8 answers

Myślę, że to naprawdę świetne pytanie, i szkoda, że zamiast zajmować się prawdziwym pytaniem, większość odpowiedzi ominęła problem i po prostu powiedziała, aby nie używać swizzling.

Używanie metody sizzling jest jak używanie ostrych noży w kuchni. Niektórzy ludzie boją się ostrych noży, ponieważ myślą, że źle się potną, ale prawda jest taka, że ostre noże są bezpieczniejsze.

Metoda swizzling może być używana do pisania lepiej, wydajniej, bardziej / align = "left" / Może być również nadużywane i prowadzić do strasznych błędów.

Tło

Jak w przypadku wszystkich wzorców projektowych, jeśli jesteśmy w pełni świadomi konsekwencji wzorca, jesteśmy w stanie podejmować bardziej świadome decyzje o tym, czy go używać. Singletony są dobrym przykładem czegoś, co jest dość kontrowersyjne , i nie bez powodu-są naprawdę trudne do prawidłowego wdrożenia. Wiele osób nadal decyduje się na użycie singletonów. To samo można powiedzieć o swizzling. Ty powinieneś stworzyć własną opinię, gdy w pełni zrozumiesz zarówno dobre, jak i złe.

Dyskusja

Oto niektóre z pułapek metody swizzling:

  • metoda nie jest atomowa
  • zmienia zachowanie kodu nie posiadanego
  • możliwe konflikty nazw
  • Swizzling zmienia argumenty metody
  • kolejność swizzles ma znaczenie
  • trudno zrozumieć (wygląda rekurencyjnie)
  • trudne do debug

Wszystkie te punkty są ważne, a zwracając się do nich, możemy poprawić zarówno nasze zrozumienie metody, jak i metodologię stosowaną do osiągnięcia rezultatu. Wezmę każdą po kolei.

Metoda nie jest atomowa

Nie widziałem jeszcze implementacji metody swizzling, która jest Bezpieczna w użyciu jednocześnie1. To nie jest problem w 95% przypadków, że chcesz użyć metody swizzling. Zazwyczaj po prostu chcesz zastąp implementację metody i chcesz, aby ta implementacja była używana przez cały okres użytkowania programu. Oznacza to, że powinieneś wykonać swoją metodę w +(void)load. Metoda klasy load jest wykonywana seryjnie na początku twojej aplikacji. Nie będziesz miał żadnych problemów ze współbieżnością, jeśli zrobisz swoje skwierczenie tutaj. Jeśli jednak będziesz miał swizzle w +(void)initialize, możesz skończyć ze stanem wyścigowym w swojej implementacji swizzling, a czas trwania może skończyć się dziwnym stan.

Zmienia zachowanie kodu nie posiadanego

To jest problem z swizzling, ale w tym rzecz. Celem jest możliwość zmiany tego kodu. Powodem, dla którego ludzie zwracają na to uwagę jako na Wielką Sprawę, jest to, że nie tylko zmieniasz rzeczy dla jednej instancji NSButton, dla której chcesz je zmienić, ale zamiast tego dla wszystkich instancji NSButton w Twojej aplikacji. Z tego powodu, należy zachować ostrożność podczas skwierczeć, ale nie trzeba go unikać / align = "left" / Pomyśl o tym w ten sposób... jeśli nadpisujesz metodę w klasie i nie wywołasz metody super class, możesz spowodować problemy. W większości przypadków Klasa super oczekuje wywołania tej metody (chyba że udokumentowano inaczej). Jeśli zastosujesz tę samą myśl do swizzling, omówiłeś większość problemów. Zawsze wywołaj oryginalną realizację. Jeśli tego nie zrobisz, prawdopodobnie zmienisz za dużo, aby być bezpiecznym.

Możliwe konflikty nazw

Nazewnictwo konflikty są problemem w całym kakao. Często prefiksujemy nazwy klas i nazwy metod w kategoriach. Niestety, konflikty nazewnicze to plaga w naszym języku. W przypadku skwierczenia, jednak nie muszą być. Musimy tylko nieco zmienić sposób myślenia o metodzie. Większość swizzling odbywa się tak:

@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

To działa dobrze, ale co by się stało, gdyby my_setFrame: zostało zdefiniowane gdzie indziej? Ten problem nie jest unikalny dla swizzling, ale możemy pracować w każdym razie wokół niego. Obejście ma dodatkową zaletę w postaci rozwiązania innych pułapek. Oto, co robimy zamiast tego:]}

@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

Chociaż wygląda to trochę mniej jak Objective-C (ponieważ używa wskaźników funkcji), pozwala uniknąć konfliktów nazewnictwa. W zasadzie robi dokładnie to samo, co standardowe skwierczenie. Może to trochę zmienić dla ludzi, którzy używają swizzling jak to zostało zdefiniowane przez jakiś czas, ale w końcu, myślę, że jest lepiej. Metoda swizzling jest zdefiniowany w ten sposób:

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

Zmiana nazwy metod zmienia argumenty metody

To jest ten wielki w moim umyśle. Z tego powodu nie należy wykonywać standardowej metody skwierczenia. Zmieniasz argumenty przekazane do implementacji oryginalnej metody. Tak to się dzieje:
[self my_setFrame:frame];

To co robi ta linia to:

objc_msgSend(self, @selector(my_setFrame:), frame);

, który użyje runtime do wyszukania implementacji my_setFrame:. Po znalezieniu implementacji wywołuje wdrożenie z tymi samymi argumentami, które zostały podane. Implementacja, którą znajduje, jest oryginalną implementacją setFrame:, więc idzie do przodu i wywołuje ją, ale argument _cmd nie jest setFrame: tak, jak powinien być. Jest teraz my_setFrame:. Oryginalna implementacja jest wywoływana z argumentem, którego nigdy się nie spodziewała. To nie jest dobre.

Jest proste rozwiązanie-użyj alternatywnej techniki swizzling zdefiniowanej powyżej. Argumenty pozostaną bez zmian!

The order of swizzles matters

Liczy się kolejność, w jakiej metody się zmieniają. Zakładając, że setFrame: jest zdefiniowane tylko na NSView, wyobraź sobie ten porządek rzeczy:

[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];

Co się dzieje, gdy metoda na NSButton jest skwierczona? Cóż, większość swizzling zapewni, że nie zastąpi implementacji setFrame: dla wszystkich widoków, więc to podciągnie metodę instancji. To wykorzysta istniejącą implementację do ponownego zdefiniowania setFrame: w klasie NSButton tak, aby wymiana implementacje nie wpływają na wszystkie widoki. Istniejącą implementacją jest ta zdefiniowana na NSView. To samo stanie się, gdy uruchomisz NSControl (ponownie używając implementacji NSView).

Kiedy wywołasz setFrame: na przycisku, wywoła on zatem Twoją metodę swizzled, a następnie przeskoczy prosto do metody setFrame: pierwotnie zdefiniowanej na NSView. Implementacje NSControl i NSView nie będą wywoływane.

Ale co by było gdyby kolejność była:

[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];

Ponieważ widok swizzling odbywa się najpierw, sterowanie swizzling będzie w stanie podciągnąć właściwą metodę. Podobnie, ponieważ swizzling sterowania był przed swizzling przycisku, przycisk pull up kontroli swizzled implementacji setFrame:. Jest to nieco mylące, ale jest to właściwa kolejność. Jak możemy zapewnić taki porządek rzeczy?

/ Align = "left" / Jeśli wkleisz load i zmienisz tylko ładowaną klasę, będziesz bezpieczna. Metoda load gwarantuje, że metoda obciążenia klasy super zostanie wywołana przed jakąkolwiek podklasą. Zdobędziemy właściwy porządek!

Trudno zrozumieć (wygląda rekurencyjnie)

Patrząc na tradycyjnie zdefiniowaną metodę, myślę, że naprawdę trudno powiedzieć, co się dzieje. Ale patrząc na alternatywny sposób, który zrobiliśmy powyżej, jest dość łatwy do zrozumienia. Ten już został rozwiązany!

Trudny do debugowania

Jeden z zamieszanie podczas debugowania polega na tym, że widzisz dziwną ścieżkę wsteczną, w której pomieszane są skwierczone nazwy i wszystko zostaje pomieszane w twojej głowie. Ponownie, alternatywna implementacja rozwiązuje ten problem. Zobaczysz wyraźnie nazwane funkcje w ścieżkach wstecznych. Mimo to, swizzling może być trudny do debugowania, ponieważ trudno jest pamiętać, jaki wpływ ma swizzling. Dobrze udokumentuj swój kod (nawet jeśli uważasz, że jesteś jedynym, który go zobaczy). Postępuj zgodnie z dobrymi praktykami, a wszystko będzie dobrze. Nie jest trudniejszy do debugowania niż kod wielowątkowy.

Podsumowanie

Metoda swizzling jest Bezpieczna, jeśli jest stosowana prawidłowo. Prosty środek bezpieczeństwa, który możesz podjąć, to tylko swizzle w load. Jak wiele rzeczy w programowaniu, może być niebezpieczne, ale zrozumienie konsekwencji pozwoli Ci używać go prawidłowo.


1 korzystając z wyżej zdefiniowanej metody swizzling, możesz uczynić rzeczy bezpiecznymi, jeśli będziesz używać trampolin. Potrzebujesz dwóch trampolin. Na początku w metodzie należy przypisać wskaźnik funkcji store do funkcji, która obracała się, dopóki nie zmienił się adres, na który wskazywano store. Pozwoli to uniknąć sytuacji, w której metoda swizzled została wywołana, zanim można było ustawić wskaźnik funkcji store. Wtedy trzeba by użyć trampoliny w przypadku, gdy implementacja nie jest jeszcze zdefiniowana w klasie i mieć trampoline lookup i wywołać metodę super class poprawnie. Zdefiniowanie metody tak, aby dynamicznie wygląda super implementacja zapewni, że kolejność połączeń nie ma znaczenia.

 409
Author: wbyoung,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-06-21 16:30:28

Najpierw zdefiniuję dokładnie co mam na myśli metodą:

  • przekierowanie wszystkich połączeń, które zostały pierwotnie wysłane do metody (o nazwie A) do nowej metody (o nazwie B).
  • Posiadamy metodę B
  • nie posiadamy metody A
  • metoda B wykonuje jakąś pracę, a następnie wywołuje metodę A.

Metoda swizzling jest bardziej ogólna niż ta, ale jest to przypadek, który mnie interesuje.

Zagrożenia:

  • Zmiany w oryginalnej klasie . We dont own the klasa, którą my skaczemy. Jeśli Klasa się zmieni, nasz swizzle może przestać działać.

  • Trudno utrzymać . Nie tylko musisz pisać i utrzymywać metodę swizzled. musisz napisać i zachować kod, który preformuje swizzle

  • Trudne do debugowania . Trudno jest podążać za przepływem swizzle, niektórzy ludzie mogą nawet nie zdawać sobie sprawy, że swizzle został wstępnie uformowany. Jeśli są błędy wprowadzone z swizzle (być może opłaty za zmiany w oryginalna Klasa) będą trudne do rozwiązania.

Podsumowując, powinieneś ograniczyć swizzle do minimum i zastanowić się, jak zmiany w oryginalnej klasie mogą wpłynąć na Twój swizzle. Powinieneś również wyraźnie komentować i dokumentować to, co robisz (lub po prostu unikać tego całkowicie).

 11
Author: Robert,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-03-18 20:38:29

To nie samo świstanie jest naprawdę niebezpieczne. Problem polega na tym, że jest on często używany do modyfikowania zachowania klas frameworkowych. Zakładając, że wiesz coś o tym, jak działają te prywatne zajęcia, jest to "niebezpieczne."Nawet jeśli Twoje modyfikacje działają dzisiaj, zawsze jest szansa, że Apple zmieni klasę w przyszłości i spowoduje, że Twoja modyfikacja się zepsuje. Ponadto, jeśli robi to wiele różnych aplikacji, znacznie utrudnia to firmie Apple zmianę frameworka bez niszczenia wielu istniejących programów.

 7
Author: Caleb,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-03-17 13:16:06

Używane ostrożnie i mądrze, może prowadzić do eleganckiego kodu, ale zwykle prowadzi do mylącego kodu.

Mówię, że powinien być zakazany, chyba że zdarzy ci się wiedzieć, że stanowi bardzo elegancką okazję do konkretnego zadania projektowego, ale musisz wyraźnie wiedzieć, dlaczego dobrze odnosi się do sytuacji i dlaczego alternatywy nie działają elegancko w tej sytuacji.

Np. dobrym zastosowaniem metody swizzling jest ISA swizzling, czyli jak obiekt implementuje wartość klucza Obserwuję.

Złym przykładem może być opieranie się na metodzie swizzling jako sposobie rozszerzania klas, co prowadzi do bardzo wysokiego sprzężenia.

 5
Author: Arafangion,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-03-17 13:16:06

Chociaż użyłem tej techniki, chciałbym zaznaczyć, że:

  • zaciemnia Twój kod, ponieważ może powodować nie udokumentowane, choć pożądane, skutki uboczne. Kiedy czyta się kod, może nie być świadomy działania efektu ubocznego, które jest wymagane, chyba że pamięta, aby przeszukać bazę kodu, aby sprawdzić, czy został on przekręcony. Nie jestem pewien, jak złagodzić ten problem, ponieważ nie zawsze jest możliwe udokumentowanie każdego miejsca, w którym kod jest zależny od strony efekt działania.
  • może to sprawić, że Twój kod będzie mniej wielokrotnego użytku, ponieważ ktoś, kto znajdzie segment kodu, który zależy od zachowania swizzled, które chciałby użyć gdzie indziej, nie może po prostu wyciąć i wkleić go do innej bazy kodu bez znajdowania i kopiowania metody swizzled.
 5
Author: user3288724,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-02-09 03:24:30

Czuję, że największym niebezpieczeństwem jest tworzenie wielu niechcianych skutków ubocznych, całkowicie przez przypadek. Te skutki uboczne mogą przedstawiać się jako "błędy", które z kolei prowadzą cię w dół złą ścieżkę, aby znaleźć rozwiązanie. Z doświadczenia wiem, że niebezpieczeństwo jest nieczytelne, mylące i frustrujące. Coś jak gdy ktoś nadużywa wskaźników funkcji w C++.

 4
Author: A.R.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-03-17 13:10:19

Możesz skończyć z dziwnie wyglądającym kodem jak

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

Z rzeczywistego kodu produkcyjnego związanego z jakąś magią interfejsu użytkownika.

 4
Author: quantumpotato,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-07-07 19:34:39

Metoda może być bardzo pomocna w testach jednostkowych.

Pozwala na napisanie makiety obiektu i użycie tego makiety zamiast rzeczywistego obiektu. Twój kod, aby pozostać czysty, a twój test jednostkowy ma przewidywalne zachowanie. Załóżmy, że chcesz przetestować kod, który używa CLLocationManager. Twój test jednostkowy może skusić startUpdatingLocation tak, że dostarczy z góry określony zestaw lokalizacji do twojego delegata i twój kod nie będzie musiał się zmieniać.

 3
Author: user3288724,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-08-09 14:34:36