Jakie są zagrożenia związane z zamianą metod w Objective-C?

235

Słyszałem, jak ludzie twierdzą, że zamiana metod jest niebezpieczną praktyką. Nawet nazwa swizzling sugeruje, że to trochę oszustwo.

Metoda Swizzling modyfikuje mapowanie, tak aby wywołanie selektora A faktycznie wywołało implementację B. Jednym z zastosowań tego jest rozszerzenie zachowania zamkniętych klas źródłowych.

Czy możemy sformalizować ryzyko, aby każdy, kto decyduje się na użycie swizzling, mógł podjąć świadomą decyzję, czy warto za to, co próbuje zrobić.

Na przykład

  • Nazewnictwo kolizji : jeśli klasa rozszerzy później swoją funkcjonalność o dodaną nazwę metody, spowoduje to wiele problemów. Zmniejsz ryzyko, rozsądnie nazywając zamiecione metody.
Robert
źródło
Nie jest już zbyt dobrze dostać coś, czego oczekujesz od kodu, który przeczytałeś
Martin Babacaev,
to brzmi jak nieczysty sposób robienia tego, co dekoratorzy Pythona robią bardzo czysto
Claudiu

Odpowiedzi:

439

Myślę, że to naprawdę świetne pytanie i szkoda, że ​​zamiast zmierzyć się z prawdziwym pytaniem, większość odpowiedzi omijała ten problem i po prostu mówiła, żeby nie używać swizzlingu.

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

Metoda swizzling może służyć do pisania lepszego, wydajniejszego i łatwiejszego do utrzymania kodu. Można go również nadużywać i prowadzić do okropnych błędów.

tło

Podobnie jak w przypadku wszystkich wzorców projektowych, jeśli jesteśmy w pełni świadomi konsekwencji tego wzoru, jesteśmy w stanie podejmować bardziej świadome decyzje dotyczące tego, czy go użyć. Singletony są dobrym przykładem czegoś, co jest dość kontrowersyjne, i nie bez powodu - są naprawdę trudne do prawidłowego wdrożenia. Jednak wiele osób nadal decyduje się na użycie singletonów. To samo można powiedzieć o swizzlingu. Powinieneś sformułować 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 swizzling nie jest atomowa
  • Zmienia zachowanie nieposiadanego kodu
  • Możliwe konflikty nazw
  • Swizzling zmienia argumenty metody
  • Kolejność zamiatania ma znaczenie
  • Trudne do zrozumienia (wygląda rekurencyjnie)
  • Trudne do debugowania

Wszystkie te punkty są ważne, a zajmując się nimi, możemy poprawić zarówno nasze rozumienie metody swizzling, jak i metodologię stosowaną do osiągnięcia wyniku. Wezmę każdy na raz.

Metoda swizzling nie jest atomowa

Nie widziałem jeszcze implementacji metody zamiatania, którą można bezpiecznie stosować jednocześnie 1 . W rzeczywistości nie stanowi to problemu w 95% przypadków, w których chcesz użyć metody swizzling. Zwykle po prostu chcesz zastąpić implementację metody i chcesz, aby ta implementacja była używana przez cały okres istnienia programu. Oznacza to, że powinieneś wykonać swoją metodę zamiatania +(void)load. Metoda loadklasy jest wykonywana szeregowo na początku aplikacji. Nie będziesz mieć żadnych problemów ze współbieżnością, jeśli wykonasz swizzling tutaj. Jeśli miałbyś się w to wpakować+(void)initialize jednak wdarł się do niego, mógłbyś skończyć z warunkami wyścigu w swizzlingowej implementacji, a środowisko wykonawcze mogłoby skończyć się w dziwnym stanie.

Zmienia zachowanie nieposiadanego kodu

Jest to problem z zamiataniem, ale o to w tym wszystkim chodzi. Celem jest możliwość zmiany tego kodu. Powodem, dla którego ludzie wskazują, że jest to wielka sprawa, jest to, że nie tylko zmieniasz rzeczy dla jednego wystąpienia NSButton, dla którego chcesz to zmienić, ale zamiast tego dla wszystkich NSButtonwystąpień w aplikacji. Z tego powodu powinieneś zachować ostrożność podczas zamiatania, ale nie musisz tego całkowicie unikać.

Pomyśl o tym w ten sposób ... jeśli przesłonisz metodę w klasie i nie wywołasz metody superklasy, możesz spowodować problemy. W większości przypadków superklasa 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 nazywaj oryginalną implementację. Jeśli nie, prawdopodobnie zmieniasz zbyt wiele, aby być bezpiecznym.

Możliwe konflikty nazw

Konflikty nazw są problemem w całej Kakao. Często poprzedzamy nazwy klas i nazwy metod w kategoriach. Niestety konflikty nazw są plagą w naszym języku. Jednak w przypadku zamiatania nie muszą tak być. Musimy tylko zmienić sposób, w jaki myślimy o zamianie metod. Większość zamiatania odbywa się w następujący sposób:

@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

Działa to dobrze, ale co by się stało, gdyby my_setFrame:zostało zdefiniowane gdzie indziej? Ten problem nie jest unikalny dla swizzowania, ale i tak możemy go obejść. To obejście ma również dodatkową zaletę 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 nieco mniej niż Objective-C (ponieważ używa wskaźników funkcji), pozwala uniknąć konfliktów nazw. Zasadniczo robi dokładnie to samo, co standardowe swizzling. Może to być niewielka zmiana dla osób, które używają swizzlingu, jak to zostało zdefiniowane przez jakiś czas, ale ostatecznie myślę, że jest lepiej. Metoda swizzling jest zdefiniowana w następujący 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

Przesuwanie przez zmianę nazw metod zmienia argumenty metody

To jest moja wielka myśl. Jest to powód, dla którego nie należy wykonywać standardowej zamiany metody. Zmieniasz argumenty przekazane do implementacji oryginalnej metody. Tak się dzieje:

[self my_setFrame:frame];

Ta linia to:

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

Który użyje środowiska wykonawczego do sprawdzenia implementacji my_setFrame:. Po znalezieniu implementacji wywołuje implementację z tymi samymi podanymi argumentami. Implementacja, którą znajduje, jest oryginalną implementacją setFrame:, więc idzie do przodu i nazywa to, ale _cmdargument nie jest taki, setFrame:jak powinien. Jest teraz my_setFrame:. Oryginalna implementacja jest wywoływana z argumentem, którego nigdy się nie spodziewała. To nie jest dobre.

Istnieje proste rozwiązanie - skorzystaj z alternatywnej techniki zamiatania zdefiniowanej powyżej. Argumenty pozostaną niezmienione!

Kolejność zamiatania ma znaczenie

Kolejność zamiatania metod ma znaczenie. Zakładając, że setFrame:zdefiniowano tylko NSView, wyobraź sobie następującą kolejność 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 dzieje się, gdy włączona NSButtonjest metoda? Cóż, większość swizzling zapewni, że nie zastąpi implementacji setFrame:dla wszystkich widoków, więc wyciągnie metodę instancji. Spowoduje to wykorzystanie istniejącej implementacji do ponownego zdefiniowania setFrame:w NSButtonklasie, aby wymiana implementacji nie wpływała na wszystkie widoki. Istniejąca implementacja jest zdefiniowana na NSView. To samo stanie się po włączeniu NSControl(ponownie przy użyciu NSViewimplementacji).

Kiedy setFrame:wciśniesz przycisk, wywoła on twoją metodę swizzled, a następnie przeskoczy prosto do setFrame:metody pierwotnie zdefiniowanej NSView. Te NSControli NSViewswizzled implementacje nie zostanie wywołana.

Ale co gdyby zamówienie było:

[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ż zamiatanie widoku odbywa się jako pierwsze, zamiatanie kontrolne będzie w stanie wyciągnąć właściwą metodę. Podobnie, ponieważ swizzling kontrola była przed przycisk swizzling, przycisk będzie podciągnąć swizzled realizację kontrolki z setFrame:. Jest to trochę mylące, ale jest to właściwa kolejność. Jak możemy zapewnić ten porządek rzeczy?

Ponownie, użyj tylko loaddo zamiatania rzeczy. Jeśli wślizgniesz się loadi wprowadzisz tylko zmiany w ładowanej klasie, będziesz bezpieczny. Te loadgwarancje metoda, super sposób obciążenie klasa zostanie wezwany przed wszelkimi podklasy. Otrzymamy dokładne zamówienie!

Trudne do zrozumienia (wygląda rekurencyjnie)

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

Trudne do debugowania

Jedną z nieporozumień podczas debugowania jest dziwny ślad, w którym zmieszane nazwy są pomieszane, a wszystko grzęźnie w twojej głowie. Ponownie, alternatywne wdrożenie rozwiązuje ten problem. Zobaczysz wyraźnie nazwane funkcje w śladach. Mimo to zamiatanie może być trudne do debugowania, ponieważ trudno jest zapamiętać, jaki wpływ ma zamiatanie. 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. Debugowanie nie jest trudniejsze niż kod wielowątkowy.

Wniosek

Metoda swizzling jest bezpieczna, jeśli jest właściwie stosowana. Prostym środkiem bezpieczeństwa, jaki możesz podjąć, jest tylko włożenie się load. Podobnie jak wiele rzeczy w programowaniu, może być niebezpieczny, ale zrozumienie konsekwencji pozwoli na prawidłowe korzystanie z niego.


1 Używając wyżej zdefiniowanej metody swizzling, możesz sprawić, że nici będą bezpieczne, jeśli będziesz używał trampolin. Potrzebujesz dwóch trampolin. Na początku metody należy przypisać wskaźnik funkcji storedo funkcji, która obraca się do momentu zmiany adresu, na który storewskazuje. Pozwoliłoby to uniknąć warunków wyścigu, w których wywołano metodę swizzled, zanim można było ustawić storewskaźnik funkcji. Będziesz wtedy musiał użyć trampoliny w przypadku, gdy implementacja nie jest jeszcze zdefiniowana w klasie i przeszukaj trampolinę i poprawnie wywołaj metodę superklasy. Zdefiniowanie metody w taki sposób, aby dynamicznie wyszukiwała super implementację, zapewni, że kolejność wywoływania połączeń nie ma znaczenia.

wbyoung
źródło
24
Niezwykle pouczająca i technicznie uzasadniona odpowiedź. Dyskusja jest naprawdę interesująca. Dziękujemy za poświęcenie czasu na napisanie tego.
Ricardo Sanchez-Saez
Czy możesz wskazać, czy w sklepach z aplikacjami jest akceptowalny efekt swizzlingu? Kieruję cię również do stackoverflow.com/questions/8834294 .
Dickey Singh,
3
App Swizzling może być używany. Wiele aplikacji i platform (w tym nasza). Wszystko powyżej nadal obowiązuje i nie możesz zamienić prywatnych metod (właściwie możesz, ale ryzykujesz odrzuceniem, a niebezpieczeństwa są inne).
wbyoung
Niesamowita odpowiedź. Ale po co ustawiać swizzling w class_swizzleMethodAndStore () zamiast bezpośrednio w + swizzle: with: store :? Dlaczego ta dodatkowa funkcja?
Frizlab
2
@Frizlab dobre pytanie! To naprawdę tylko styl. Jeśli piszesz sporo kodu, który działa bezpośrednio z interfejsem API środowiska wykonawczego Objective-C (w C), fajnie jest móc nazwać go stylem C w celu zachowania spójności. Jedynym powodem, dla którego mogę o tym pomyśleć, jest to, że jeśli napisałeś coś w czystym C, to jest to jeszcze bardziej możliwe do wywołania. Nie ma jednak powodu, dla którego nie można tego wszystkiego zrobić w metodzie Objective-C.
wbyoung
12

Najpierw zdefiniuję dokładnie, co mam na myśli przez metodę swizzling:

  • Przekierowanie wszystkich wywołań pierwotnie wysłanych do metody (zwanej A) na nową metodę (zwaną B).
  • Posiadamy metodę B.
  • Nie posiadamy metody A.
  • Metoda B wykonuje pewną pracę, a następnie wywołuje metodę A.

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

Niebezpieczeństwa

  • Zmiany w oryginalnej klasie . Nie jesteśmy właścicielami klasy, którą zamiatamy. Jeśli klasa się zmieni, nasz swizzle może przestać działać.

  • Trudne w utrzymaniu . Nie tylko musisz pisać i utrzymywać metodę swizzled. musisz napisać i utrzymywać kod, który wykonuje swizzle

  • Trudno debugować . Trudno jest śledzić przepływ swizzle, niektórzy ludzie mogą nawet nie zdawać sobie sprawy, że swizzle został już wykonany. Jeśli pojawią się błędy ze swizzle (być może spowodowane zmianami w oryginalnej klasie), będzie trudno je rozwiązać.

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

Robert
źródło
@everyone Połączyłem twoje odpowiedzi w niezłym formacie. Edytuj / dodaj do niego. Dzięki za wkład.
Robert
Jestem fanem twojej psychologii. „Z wielką mocą swizzlingu wiąże się ogromna odpowiedzialność swizzlingu”.
Jacksonkr,
7

To nie samo wirowanie jest naprawdę niebezpieczne. Problem polega na tym, że, jak mówisz, często używany jest do modyfikowania zachowania klas frameworka. Zakłada się, że wiesz coś o tym, jak działają te prywatne klasy, co jest „niebezpieczne”. Nawet jeśli twoje modyfikacje działają dzisiaj, zawsze istnieje 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, Apple znacznie utrudnia zmianę frameworka bez niszczenia wielu istniejących programów.

Caleb
źródło
Powiedziałbym, że tacy deweloperzy powinni czerpać z tego, co sieją - ściśle powiązali swój kod z konkretną implementacją. Dlatego to ich wina, że ​​ta implementacja zmienia się w subtelny sposób, który nie powinien złamać ich kodu, gdyby nie ich wyraźna decyzja, aby połączyć kod tak ściśle, jak oni.
Arafangion,
Jasne, ale powiedzmy, że bardzo popularna aplikacja psuje się w następnej wersji iOS. Łatwo powiedzieć, że to wina programisty i powinni wiedzieć lepiej, ale to sprawia, że ​​Apple źle wygląda w oczach użytkownika. Nie widzę żadnej szkody w zamianie własnych metod, a nawet klas, jeśli chcesz to zrobić, poza tym, że może to sprawić, że kod będzie nieco mylący. Zaczyna się robić trochę bardziej ryzykownie, gdy zamiatasz kod kontrolowany przez kogoś innego - i właśnie w tej sytuacji zamiatanie wydaje się najbardziej kuszące.
Caleb,
Apple nie jest jedynym dostawcą klas podstawowych, które można modyfikować poprzez siwzzling. Twoja odpowiedź powinna zostać zredagowana, aby uwzględnić wszystkich.
AR
@AR Być może, ale pytanie jest oznaczone iOS i iPhone, więc powinniśmy rozważyć to w tym kontekście. Biorąc pod uwagę, że aplikacje iOS muszą statycznie łączyć dowolne biblioteki i frameworki inne niż te dostarczane przez iOS, Apple jest w rzeczywistości jedynym podmiotem, który może zmienić implementację klas frameworków bez udziału deweloperów aplikacji. Ale zgadzam się z twierdzeniem, że podobne problemy wpływają na inne platformy, i powiedziałbym, że porady można uogólnić poza swizzowaniem. Ogólnie rzecz biorąc, rada brzmi: trzymaj ręce przy sobie. Unikaj modyfikowania komponentów obsługiwanych przez inne osoby.
Caleb,
Metoda zamiatania może być tak niebezpieczna. ale kiedy pojawiają się frameworki, nie omijają całkowicie poprzednich. wciąż musisz zaktualizować podstawową strukturę, której oczekuje Twoja aplikacja. i ta aktualizacja spowodowałaby uszkodzenie Twojej aplikacji. aktualizacja systemu iOS prawdopodobnie nie będzie tak dużym problemem. Mój wzorzec użycia polegał na zastąpieniu domyślnego identyfikatora ponownego użycia. Ponieważ nie miałem dostępu do zmiennej _reuseIdentifier i nie chciałem jej zastępować, chyba że nie pod warunkiem, że moim jedynym rozwiązaniem było podklasowanie każdego z nich i podanie identyfikatora ponownego użycia lub zamiana i zastąpienie, jeśli zero. Kluczem jest typ implementacji ...
The Lazy Coder
5

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

Mówię, że należy go zbanować, chyba że wiesz, że stanowi on bardzo elegancką okazję do wykonania określonego zadania projektowego, ale musisz wyraźnie wiedzieć, dlaczego dobrze pasuje do sytuacji i dlaczego alternatywy nie działają elegancko w tej sytuacji .

Np. Jednym dobrym zastosowaniem metody swizzling jest swizzling, w ten sposób ObjC implementuje Key Value Observing.

Zły przykład może polegać na zamianie metod jako sposobu na rozszerzenie twoich klas, co prowadzi do bardzo wysokiego sprzężenia.

Arafangion
źródło
1
-1 za wzmiankę o KVO - w kontekście zamiany metody . isa swizzling to coś innego.
Nikolai Ruhe
@NikolaiRuhe: Prosimy o dostarczanie referencji, dzięki czemu będę mógł zostać oświecony.
Arafangion
Oto odniesienie do szczegółów implementacji KVO . Metoda swizzling jest opisana na CocoaDev.
Nikolai Ruhe
5

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

  • Ukrywa twój kod, ponieważ może powodować nieudokumentowane, choć pożądane, skutki uboczne. Kiedy ktoś czyta kod, może nie być świadomy działania niepożądanego, które jest wymagane, chyba że pamięta, aby przeszukać bazę kodu, aby sprawdzić, czy został zamieciony. 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 zachwianego efektu ubocznego.
  • Może sprawić, że Twój kod będzie mniej przydatny do ponownego użycia, ponieważ ktoś, kto znajdzie fragment kodu zależny od zachowanego kodu, którego chciałby użyć w innym miejscu, nie może po prostu wyciąć go i wkleić do innej bazy kodu bez znalezienia i skopiowania zamienionej metody.
użytkownik3288724
źródło
4

Uważam, że największym niebezpieczeństwem jest tworzenie wielu niepożądanych efektów ubocznych, zupełnie przypadkowo. Te działania niepożądane mogą przedstawiać się jako „błędy”, które z kolei prowadzą cię na niewłaściwą ścieżkę do znalezienia rozwiązania. Z mojego doświadczenia wynika, że ​​niebezpieczeństwo jest nieczytelne, zagmatwane i frustrujące. To tak, jakby ktoś nadużywał wskaźników funkcji w C ++.

AR
źródło
Ok, więc problem polega na tym, że trudno jest debugować. Przyczyną problemów jest to, że recenzenci nie zdają sobie sprawy z tego, że kod został zawirowany i szukają niewłaściwego miejsca podczas debugowania.
Robert
3
Tak właściwie. Ponadto, gdy jest nadużywany, możesz dostać się do niekończącego się łańcucha lub sieci swizzli. Wyobraź sobie, co może się stać, gdy zmienisz tylko jedną z nich w przyszłości? Porównuję to doświadczenie do układania filiżanek lub grania w „Code Jenga”
AR
4

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

- (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 magią interfejsu użytkownika.

quantumpotato
źródło
3

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

Pozwala napisać próbny obiekt i użyć tego próbnego obiektu zamiast rzeczywistego obiektu. Twój kod powinien pozostać czysty, a test jednostkowy ma przewidywalne zachowanie. Powiedzmy, że chcesz przetestować kod korzystający z CLLocationManager. Twój test jednostkowy mógłby zamienić startUpdatingLocation tak, aby dostarczał delegatowi z góry określony zestaw lokalizacji, a Twój kod nie musiałby się zmieniać.

użytkownik3288724
źródło
isa-swizzling byłby lepszym wyborem.
vikingosegundo