Method Swizzling — техніка runtime, при якій реалізації двох методів класу міняються місцями під час виконання. Вона дозволяє перевизначити або доповнити поведінку системного методу без створення підкласу та без зміни вихідного коду. Найбільше застосування техніка отримала в iOS-розробці на Objective-C, але аналоги існують в Kotlin/Android через reflection. За даними NSHipster Guide by Mattt, 2024, swizzling — один з найпотужніших, але й найнебезпечніших механізмів Objective-C Runtime.
Головне
Method Swizzling — runtime-техніка, що міняє місцями реалізації двох методів Objective-C. Після swizzling виклик originalSelector призводить до виконання коду swizzledSelector, і навпаки. Це можливо завдяки архітектурі Objective-C Runtime, де кожен селектор (SEL) пов’язаний з реалізацією (IMP) через dispatch table — таблицю, яку можна модифікувати під час виконання.
Термін «swizzling» введений у спільноті Cocoa розробників на початку 2000-х років. Широкої популярності техніка набула завдяки бібліотекам: AFNetworking (swizzling UIWebView для відстеження завантаження), Aspects (AOP-фреймворк на основі swizzling) та FLEX (інструмент налагодження, що swizzle-ить системні методи для інспекції). Сьогодні swizzling використовується в більшості iOS-додатків неявно — через бібліотеки моніторингу та аналітики.
Важлива властивість swizzling — глобальність: заміна реалізації відбувається на рівні класу, а не екземпляра. Якщо бібліотека swizzle-ить метод UIViewController.viewDidLoad, це впливає на ВСІ екземпляри UIViewController в додатку, включаючи системні. Це одночасно сила swizzling — один рядок коду змінює поведінку додатку цілком — і головне джерело багів.
Objective-C Runtime зберігає в кожному класі dispatch table — словник, де ключ — SEL (ідентифікатор методу), значення — IMP (покажчик на функцію-реалізацію). Коли додаток надсилає повідомлення об’єкту, objc_msgSend виконує лінійний пошук по цій таблиці. Method Swizzling замінює IMP в одного SEL на IMP іншого SEL, перенаправляючи виклики.
// Реалізація безпечного 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
Ключова функція — method_exchangeImplementations(Method, Method). Вона атомарно міняє IMP у двох Method-об’єктів. Після виклику dispatch table класу змінена: при зверненні до original виконується код swizzled, при зверненні до swizzled — код original. Категорія SafeSwizzle додає цей метод до всіх NSObject, дозволяючи будь-якому класу виконати swizzling.
Безпечна реалізація swizzling вимагає виклику original implementation всередині swizzled-версії. Інакше оригінальна поведінка методу втрачається безповоротно. Правильний патерн — зберегти original IMP до обміну і викликати його в swizzled-методі:
// Swizzling з викликом оригінальної реалізації
- (void)swizzled_viewDidLoad {
// 1. Виклик оригінальної реалізації
[self swizzled_viewDidLoad];
// 2. Додаткова логіка після оригінального виклику
NSLog("viewDidLoad виконано, swizzling активний");
}
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzleClassMethod:@selector(viewDidLoad)
with:@selector(swizzled_viewDidLoad)];
});
}
dispatch_once гарантує, що swizzling виконається рівно один раз за час життя додатку. Повторний swizzling того ж методу призведе до нескінченної рекурсії: swizzled метод буде викликати сам себе. +load викликається при завантаженні класу в runtime — це безпечна точка для swizzling, що виконується до основного коду додатку.
Dispatch table Objective-C класу — це масив структур method_t, що містять SEL, IMP та тип значення, що повертається. method_exchangeImplementations просто міняє два покажчики IMP в цій таблиці. Важливо: swizzling працює лише на рівні класу, не протоколу. Якщо метод визначений в протоколі, але не реалізований — dispatch table не містить запису для swizzling.
Overhead swizzling мінімальний — обмін двох покажчиків IMP в dispatch table займає кілька наносекунд. Після swizzling виклик методу не сповільнюється: objc_msgSend знаходить IMP за той же O(1) час, що й до swizzling. Єдина додаткова операція — перевірка method cache при першому виклику після обміну. За даними Apple Performance Team, swizzling не впливає на продуктивність додатку.
Method Swizzling використовується в трьох основних сценаріях: моніторинг та аналітика (відстеження viewDidLoad, viewDidAppear для автоматичного надсилання подій), AOP-перехоплення (логування параметрів всіх викликів методу) та hotfix (виправлення багу в production без App Store Review через бібліотеки на кшталт JSPatch).
Кожен з цих сценаріїв працює завдяки тому, що swizzling застосовується централізовано. Бібліотека аналітики виконує swizzling один раз в +load, і всі UIViewController в додатку починають надсилати події. Розробнику не потрібно додавати код в кожен контролер — це знижує дублювання та ризик помилок.
На Android method swizzling в класичному сенсі Objective-C неможливий — Java/Kotlin використовують статичну диспетчеризацію через vtable. Однак існують механізми, що досягають аналогічного ефекту: Java Reflection для заміни реалізації в runtime та Gradle Transform API / ASM для модифікації байт-коду на етапі збірки.
// Swizzling на Android через reflection + companion object
class Logger {
companion object {
var originalImpl: (() -> Unit)? = null
}
fun log() {
println("оригінальний лог")
}
}
// Заміна реалізації в runtime через reflection
fun swizzleLog() {
val originalMethod = Logger::class.java
.getDeclaredMethod("log")
originalMethod.isAccessible = true
Logger.originalImpl = {
originalMethod.invoke(Logger())
}
// Підміна через inline функцію
println("swizzled: лог перехоплено")
}
Цей код замінює поведінку методу log() через Java Reflection: getDeclaredMethod отримує доступ до приватної реалізації, isAccessible вимикає перевірку доступу. Замість прямого виклику log() викликається обгортка, що виконує додаткову логіку. Однак Android оптимізує гарячі методи через JIT — reflection може не спрацювати на вже скомпільованих AOT-ділянках.
Більш надійний підхід — bytecode manipulation через Gradle Transform API або AGP (Android Gradle Plugin) з ASM-бібліотекою. Модифікація байт-коду виконується на етапі компіляції: ASM додає виклики в кожен метод класу. Так працюють інструменти покриття коду (JaCoCo) та моніторингу продуктивності (Firebase Performance Monitoring).
Method Swizzling — техніка з високими ризиками. Конфлікти між бібліотеками: якщо дві бібліотеки swizzle-ють один і той же метод, порядок виконання не гарантований. Несумісність з оновленнями iOS: якщо Apple змінює сигнатуру або видаляє метод в новій версії iOS, swizzling призводить до crash. Відсутність видимості в коді: swizzling не видно в реалізації класу, що ускладнює налагодження.
| Ризик | Опис | Мітигація |
|---|---|---|
| Конфлікт бібліотек | Дві бібліотеки swizzle-ють viewDidAppear — одна ламає іншу | Перевіряти, чи swizzlen вже метод, через class_getInstanceMethod |
| Рекурсія | Повторний swizzling того ж методу викликає нескінченний цикл | Завжди використовувати dispatch_once |
| Зміна сигнатури | Apple змінює сигнатуру методу в новій iOS — IMP не збігається | Тестувати на всіх підтримуваних версіях iOS |
| Невидимість | Swizzling не відображається в стеку викликів Xcode | Документувати всі swizzling-операції в коді |
| App Review | Apple відхиляє додатки з недокументованим swizzling | Використовувати тільки публічні API та документувати призначення |
Best practices для безпечного swizzling включають: завжди викликати оригінальну реалізацію, виконувати swizzling строго в +load через dispatch_once, іменувати swizzled-методи з префіксом (наприклад, s_originalMethodName), документувати кожну swizzling-операцію із зазначенням мети. Бібліотека Aspects вирішує проблему конфліктів через ланцюжкове виконання блоків до/після оригінального методу.
Альтернативи method swizzling є кращими для production-коду через передбачуваність та безпеку. Делегати та протоколи (UIApplicationDelegate, UITableViewDelegate) надають явні точки розширення без модифікації runtime. Субкласинг — створення підкласу UIViewController з перевизначенням viewDidAppear — працює передбачувано і не має конфліктів.
SwiftUI та Combine усувають необхідність swizzling: модифікатори (onAppear, onChange) додають поведінку декларативно, без перевизначення методів. В Android Jetpack Compose досягає того ж через ефекти (LaunchedEffect, SideEffect) та модифікатори. AOP-фреймворки (AspectJ для Android, InterposeKit для iOS) надають безпечну альтернативу з compile-time weaving.
За даними Apple WWDC 2024, Swift runtime не підтримує method swizzling на рівні мови — @objc dynamic методи можуть бути swizzlen тільки через Objective-C Runtime. Swift-додатки, що не використовують @objc, повністю захищені від випадкового swizzling сторонніми бібліотеками. Це робить Swift безпечнішим, але обмежує можливості runtime-інструментування.
SwiftUI модифікатори (onAppear, onChange, onReceive) та Jetpack Compose ефекти (LaunchedEffect, SideEffect, DisposableEffect) повністю замінюють swizzling для UI-задач. Вони надають декларативний, передбачуваний та перевірений спосіб додавання наскрізної поведінки без модифікації dispatch table. В нових проєктах Apple та Google рекомендують саме цей підхід замість runtime-перехоплення.
Часті запитання
Method Swizzling прийнятний для production при дотриманні правил: dispatch_once для одноразового виконання, виклик оригінальної реалізації, тестування на всіх версіях iOS та документування. Для простих завдань краще використовувати делегати або субкласинг. Swizzling в production виправданий для бібліотек моніторингу та аналітики.
Method Swizzling — конкретна техніка заміни IMP в dispatch table. AOP (Aspect-Oriented Programming) — парадигма, в якій swizzling може використовуватися як один з механізмів. AOP включає також compile-time weaving (AspectJ), proxy-based перехоплення (Spring AOP) та code generation.
Використовувати breakpoint в objc_msgSend для відстеження всіх повідомлень. Додати символічний breakpoint на method_exchangeImplementations з умовою на ім’я класу. Інструмент FLEX показує, які методи класу swizzlen. Для систематичної перевірки використовувати lldb-скрипт, що виводить dispatch table класу.
Swift не підтримує swizzling на рівні мови. Method Swizzling працює тільки для методів, позначених @objc dynamic, які компілюються через Objective-C Runtime. Чисті Swift-методи (без @objc) використовують статичну диспетчеризацію і не можуть бути swizzlen — їх dispatch table недоступна для модифікації.
Firebase Analytics (swizzling viewDidAppear для automatic screen tracking), Amplitude, Mixpanel, FLEX (інспекція UI), OHHTTPStubs (мокування мережевих запитів), Aspects (AOP-фреймворк). Всі вони виконують swizzling в +load через dispatch_once з викликом оригінальної реалізації.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також