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 load
klasy 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 NSButton
wystą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 _cmd
argument 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 NSButton
jest 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 NSButton
klasie, 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 NSView
implementacji).
Kiedy setFrame:
wciśniesz przycisk, wywoła on twoją metodę swizzled, a następnie przeskoczy prosto do setFrame:
metody pierwotnie zdefiniowanej NSView
. Te NSControl
i NSView
swizzled 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 load
do zamiatania rzeczy. Jeśli wślizgniesz się load
i wprowadzisz tylko zmiany w ładowanej klasie, będziesz bezpieczny. Te load
gwarancje 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 store
do funkcji, która obraca się do momentu zmiany adresu, na który store
wskazuje. Pozwoliłoby to uniknąć warunków wyścigu, w których wywołano metodę swizzled, zanim można było ustawić store
wskaź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.