Method Swizzling — technika runtime, w której implementacje dwóch metod klasy zamieniają się miejscami podczas wykonania. Pozwala nadpisać lub rozszerzyć zachowanie systemowej metody bez tworzenia podklasy i bez zmiany kodu źródłowego. Największe zastosowanie technika znalazła w iOS-rozwoju w Objective-C, ale analogi istnieją w Kotlin/Android przez reflection. Według NSHipster Guide by Mattt, 2024, swizzling — jeden z najpotężniejszych, ale i najniebezpieczniejszych mechanizmów Objective-C Runtime.
Najważniejsze
Method Swizzling — technika runtime, zamieniająca miejscami implementacje dwóch metod Objective-C. Po swizzlingu wywołanie originalSelector prowadzi do wykonania kodu swizzledSelector, i odwrotnie. Jest to możliwe dzięki architekturze Objective-C Runtime, gdzie każdy selektor (SEL) jest powiązany z implementacją (IMP) przez dispatch table — tabelę, którą można modyfikować podczas wykonania.
Termin «swizzling» został wprowadzony w społeczności Cocoa developerów na początku lat 2000. Szeroką popularność technika zyskała dzięki bibliotekom: AFNetworking (swizzling UIWebView do śledzenia ładowania), Aspects (AOP-framework oparty na swizzlingu) i FLEX (narzędzie debugowania, swizzlujące systemowe metody do inspekcji). Dziś swizzling jest używany w większości aplikacji iOS niejawnie — przez biblioteki monitorowania i analityki.
Ważna właściwość swizzlingu — globalność: zamiana implementacji następuje na poziomie klasy, a nie instancji. Jeśli biblioteka swizzluje metodę UIViewController.viewDidLoad, wpływa to na WSZYSTKIE instancje UIViewController w aplikacji, łącznie z systemowymi. To jednocześnie siła swizzlingu — jedna linia kodu zmienia zachowanie całej aplikacji — i główne źródło błędów.
Objective-C Runtime przechowuje w każdej klasie dispatch table — słownik, gdzie klucz to SEL (identyfikator metody), a wartość to IMP (wskaźnik na funkcję implementacji). Gdy aplikacja wysyła wiadomość do obiektu, objc_msgSend wykonuje liniowe wyszukiwanie w tej tabeli. Method Swizzling zastępuje IMP jednego SEL na IMP innego SEL, przekierowując wywołania.
// Implementacja bezpiecznego method swizzling
@implementation NSObject (SafeSwizzle)
+ (void)swizzleClassMethod:(SEL)original
with:(SEL)swizzled {
Class cls = [self class];
SEL originalSel = original;
SEL swizzledSel = swizzled;
Method originalMethod = class_getInstanceMethod(cls, originalSel);
Method swizzledMethod = class_getInstanceMethod(cls, swizzledSel);
method_exchangeImplementations(originalMethod, swizzledMethod);
}
@end
Kluczowa funkcja — method_exchangeImplementations(Method, Method). Atomowo zamienia IMP dwóch obiektów Method. Po wywołaniu dispatch table klasy jest zmieniona: przy dostępie do original wykonywany jest kod swizzled, przy dostępie do swizzled — kod original. Kategoria SafeSwizzle dodaje tę metodę do wszystkich NSObject, umożliwiając każdej klasie wykonanie swizzlingu.
Bezpieczna implementacja swizzlingu wymaga wywołania original implementation wewnątrz swizzled-wersji. W przeciwnym razie oryginalne zachowanie metody jest bezpowrotnie tracone. Prawidłowy wzorzec — zapisać oryginalny IMP przed zamianą i wywoływać go w swizzled-metodzie:
// Swizzling z wywołaniem oryginalnej implementacji
- (void)swizzled_viewDidLoad {
// 1. Wywołanie oryginalnej implementacji
[self swizzled_viewDidLoad];
// 2. Dodatkowa logika po oryginalnym wywołaniu
NSLog("viewDidLoad wykonany, swizzling aktywny");
}
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzleClassMethod:@selector(viewDidLoad)
with:@selector(swizzled_viewDidLoad)];
});
}
dispatch_once gwarantuje, że swizzling wykona się dokładnie raz w ciągu życia aplikacji. Ponowny swizzling tej samej metody doprowadzi do nieskończonej rekurencji: swizzled metoda będzie wywoływać samą siebie. +load jest wywoływany przy ładowaniu klasy do runtime — to bezpieczny punkt do swizzlingu, wykonujący się przed głównym kodem aplikacji.
Dispatch table klasy Objective-C — to tablica struktur method_t, zawierających SEL, IMP i typ wartości zwracanej. method_exchangeImplementations po prostu zamienia dwa wskaźniki IMP w tej tabeli. Ważne: swizzling działa tylko na poziomie klasy, nie protokołu. Jeśli metoda jest zdefiniowana w protokole, ale nie zaimplementowana — dispatch table nie zawiera wpisu do swizzlingu.
Narzut swizzlingu jest minimalny — zamiana dwóch wskaźników IMP w dispatch table zajmuje kilka nanosekund. Po swizzlingu wywołanie metody nie spowalnia się: objc_msgSend znajduje IMP w tym samym czasie O(1), co przed swizzlingiem. Jedyna dodatkowa operacja — sprawdzenie method cache przy pierwszym wywołaniu po zamianie. Według Apple Performance Team, swizzling nie wpływa na wydajność aplikacji.
Method Swizzling jest używany w trzech głównych scenariuszach: monitorowanie i analityka (śledzenie viewDidLoad, viewDidAppear do automatycznego wysyłania zdarzeń), AOP-przechwytywanie (logowanie parametrów wszystkich wywołań metody) i hotfix (naprawa błędu w production bez App Store Review przez biblioteki takie jak JSPatch).
Każdy z tych scenariuszy działa dzięki temu, że swizzling jest stosowany centralnie. Biblioteka analityki wykonuje swizzling raz w +load, a wszystkie UIViewController w aplikacji zaczynają wysyłać zdarzenia. Deweloper nie musi dodawać kodu w każdym kontrolerze — to zmniejsza duplikację i ryzyko błędów.
Na Androidzie method swizzling w klasycznym sensie Objective-C jest niemożliwy — Java/Kotlin używają statycznej dyspozytoryzacji przez vtable. Istnieją jednak mechanizmy osiągające podobny efekt: Java Reflection do zamiany implementacji w runtime oraz Gradle Transform API / ASM do modyfikacji bajtkodu na etapie budowy.
// Swizzling na Androidzie przez reflection + companion object
class Logger {
companion object {
var originalImpl: (() -> Unit)? = null
}
fun log() {
println("oryginalny log")
}
}
// Zamiana implementacji w runtime przez reflection
fun swizzleLog() {
val originalMethod = Logger::class.java
.getDeclaredMethod("log")
originalMethod.isAccessible = true
Logger.originalImpl = {
originalMethod.invoke(Logger())
}
// Podmiana przez funkcję inline
println("swizzled: log przechwycony")
}
Ten kod zastępuje zachowanie metody log() przez Java Reflection: getDeclaredMethod uzyskuje dostęp do prywatnej implementacji, isAccessible wyłącza sprawdzanie dostępu. Zamiast bezpośredniego wywołania log() wywoływana jest otoczka wykonująca dodatkową logikę. Jednak Android optymalizuje gorące metody przez JIT — reflection może nie zadziałać na już skompilowanych fragmentach AOT.
Bardziej niezawodne podejście — bytecode manipulation przez Gradle Transform API lub AGP (Android Gradle Plugin) z biblioteką ASM. Modyfikacja bajtkodu jest wykonywana na etapie kompilacji: ASM dodaje wywołania w każdej metodzie klasy. Tak działają narzędzia pokrycia kodu (JaCoCo) i monitorowania wydajności (Firebase Performance Monitoring).
Method Swizzling — technika o wysokim ryzyku. Konflikty między bibliotekami: jeśli dwie biblioteki swizzlują tę samą metodę, kolejność wykonania nie jest gwarantowana. Niezgodność z aktualizacjami iOS: jeśli Apple zmienia sygnaturę lub usuwa metodę w nowej wersji iOS, swizzling prowadzi do crasha. Brak widoczności w kodzie: swizzling nie jest widoczny w implementacji klasy, co utrudnia debugowanie.
| Ryzyko | Opis | Mitigacja |
|---|---|---|
| Konflikt bibliotek | Dwie biblioteki swizzlują viewDidAppear — jedna łamie drugą | Sprawdzić, czy metoda nie jest już swizzlowana przez class_getInstanceMethod |
| Rekurencja | Ponowny swizzling tej samej metody powoduje nieskończoną pętlę | Zawsze używać dispatch_once |
| Zmiana sygnatury | Apple zmienia sygnaturę metody w nowej iOS — IMP nie pasuje | Testować na wszystkich wspieranych wersjach iOS |
| Niewidoczność | Swizzling nie wyświetla się w stosie wywołań Xcode | Dokumentować wszystkie operacje swizzlingu w kodzie |
| App Review | Apple odrzuca aplikacje z nieudokumentowanym swizzlingiem | Używać tylko publicznych API i dokumentować przeznaczenie |
Best practices dla bezpiecznego swizzlingu obejmują: zawsze wywoływać oryginalną implementację, wykonywać swizzling ściśle w +load przez dispatch_once, nazywać swizzled-metody z prefiksem (np. s_originalMethodName), dokumentować każdą operację swizzlingu z podaniem celu. Biblioteka Aspects rozwiązuje problem konfliktów poprzez łańcuchowe wykonanie bloków przed/po oryginalnej metodzie.
Alternatywy dla method swizzling są preferowane w kodzie produkcyjnym ze względu na przewidywalność i bezpieczeństwo. Delegaty i protokoły (UIApplicationDelegate, UITableViewDelegate) zapewniają jawne punkty rozszerzeń bez modyfikacji runtime. Subklasy — tworzenie podklasy UIViewController z nadpisaniem viewDidAppear — działa przewidywalnie i nie ma konfliktów.
SwiftUI i Combine eliminują potrzebę swizzlingu: modyfikatory (onAppear, onChange) dodają zachowanie deklaratywnie, bez nadpisywania metod. W Androidzie Jetpack Compose osiąga to samo przez efekty (LaunchedEffect, SideEffect) i modyfikatory. AOP-frameworki (AspectJ dla Androida, InterposeKit dla iOS) zapewniają bezpieczną alternatywę z compile-time weaving.
Według Apple WWDC 2024, Swift runtime nie wspiera method swizzlingu na poziomie języka — @objc dynamic metody mogą być swizzlowane tylko przez Objective-C Runtime. Aplikacje Swift, nie używające @objc, są w pełni chronione przed przypadkowym swizzlingiem przez biblioteki stron trzecich. To czyni Swifta bezpieczniejszym, ale ogranicza możliwości runtime-instrumentowania.
SwiftUI modyfikatory (onAppear, onChange, onReceive) i Jetpack Compose efekty (LaunchedEffect, SideEffect, DisposableEffect) całkowicie zastępują swizzling dla zadań UI. Zapewniają deklaratywny, przewidywalny i testowalny sposób dodawania przekrojowego zachowania bez modyfikacji dispatch table. W nowych projektach Apple i Google zalecają właśnie to podejście zamiast przechwytywania w runtime.
Często zadawane pytania
Method Swizzling jest akceptowalny dla production przy przestrzeganiu zasad: dispatch_once do jednorazowego wykonania, wywołanie oryginalnej implementacji, testowanie na wszystkich wersjach iOS i dokumentowanie. Do prostych zadań lepiej używać delegatów lub subklasingu. Swizzling w production jest uzasadniony dla bibliotek monitorowania i analityki.
Method Swizzling — konkretna technika zamiany IMP w dispatch table. AOP (Aspect-Oriented Programming) — paradygmat, w którym swizzling może być używany jako jeden z mechanizmów. AOP obejmuje również compile-time weaving (AspectJ), proxy-based przechwytywanie (Spring AOP) i code generation.
Użyć breakpoint w objc_msgSend do śledzenia wszystkich wiadomości. Dodać symboliczny breakpoint na method_exchangeImplementations z warunkiem na nazwę klasy. Narzędzie FLEX pokazuje, które metody klasy są swizzlowane. Do systematycznej kontroli użyć skryptu lldb, wyświetlającego dispatch table klasy.
Swift nie wspiera swizzlingu na poziomie języka. Method Swizzling działa tylko dla metod oznaczonych @objc dynamic, które są kompilowane przez Objective-C Runtime. Czyste metody Swift (bez @objc) używają statycznej dyspozytoryzacji i nie mogą być swizzlowane — ich dispatch table jest niedostępna do modyfikacji.
Firebase Analytics (swizzling viewDidAppear do automatycznego śledzenia ekranów), Amplitude, Mixpanel, FLEX (inspekcja UI), OHHTTPStubs (mockowanie zapytań sieciowych), Aspects (AOP-framework). Wszystkie wykonują swizzling w +load przez dispatch_once z wywołaniem oryginalnej implementacji.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również