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 امکانپذیر است، جایی که هر selector (SEL) از طریق dispatch table — جدولی که میتوان در زمان اجرا تغییر داد — به پیادهسازی (IMP) متصل است.
اصطلاح «swizzling» در اوایل دهه 2000 در جامعه توسعهدهندگان Cocoa معرفی شد. این تکنیک به لطف کتابخانههای زیر شهرت گستردهای یافت: AFNetworking (swizzling UIWebView برای ردیابی بارگذاری)، Aspects (چارچوب AOP مبتنی بر swizzling) و FLEX (ابزار اشکالزدایی که متدهای سیستمی را برای بازرسی swizzle میکند). امروزه swizzling در اکثر برنامههای iOS به صورت ضمنی — از طریق کتابخانههای مانیتورینگ و آنالیتیکس — استفاده میشود.
ویژگی مهم swizzling — جهانی بودن: جایگزینی پیادهسازی در سطح کلاس اتفاق میافتد، نه نمونه. اگر کتابخانهای متد UIViewController.viewDidLoad را swizzle کند، این روی تمام نمونههای 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 دارد. در غیر این صورت، رفتار اصلی متد به طور غیرقابل برگشتی از دست میرود. الگوی صحیح — ذخیره 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 نیست.
سربار 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 به صورت متمرکز اعمال میشود. کتابخانه آنالیتیکس یک بار در +load swizzling را انجام میدهد و تمام 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("log اصلی")
}
}
// جایگزینی پیادهسازی در runtime از طریق reflection
fun swizzleLog() {
val originalMethod = Logger::class.java
.getDeclaredMethod("log")
originalMethod.isAccessible = true
Logger.originalImpl = {
originalMethod.invoke(Logger())
}
// جایگزینی از طریق تابع inline
println("swizzled: log رهگیری شد")
}
این کد رفتار متد 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 منجر به کرش میشود. عدم دید در کد: swizzling در پیادهسازی کلاس قابل مشاهده نیست که اشکالزدایی را دشوار میکند.
| ریسک | توضیحات | کاهش |
|---|---|---|
| تداخل کتابخانهها | دو کتابخانه viewDidAppear را swizzle میکنند — یکی دیگری را خراب میکند | بررسی کنید که آیا متد قبلاً از طریق class_getInstanceMethod swizzle شده است |
| بازگشت | 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 ترجیح داده میشوند. Delegateها و پروتکلها (UIApplicationDelegate, UITableViewDelegate) نقاط گسترش صریح بدون تغییر runtime فراهم میکنند. زیرکلاسسازی — ایجاد زیرکلاس UIViewController با بازنویسی viewDidAppear — به صورت قابل پیشبینی کار میکند و تداخل ندارد.
SwiftUI و Combine نیاز به swizzling را از بین میبرند: modifierها (onAppear, onChange) رفتار را به صورت اعلامی و بدون بازنویسی متدها اضافه میکنند. در Android، Jetpack Compose از طریق effectها (LaunchedEffect, SideEffect) و modifierها به همان نتیجه میرسد. چارچوبهای AOP (AspectJ برای Android، InterposeKit برای iOS) جایگزین ایمن با compile-time weaving ارائه میدهند.
به گفته Apple WWDC 2024، Swift runtime از method swizzling در سطح زبان پشتیبانی نمیکند — متدهای @objc dynamic فقط از طریق Objective-C Runtime میتوانند swizzle شوند. برنامههای Swift که از @objc استفاده نمیکنند، به طور کامل از swizzling تصادفی توسط کتابخانههای شخص ثالث محافظت میشوند. این Swift را امنتر میکند، اما قابلیتهای ابزارسازی runtime را محدود میکند.
SwiftUI modifierها (onAppear, onChange, onReceive) و Jetpack Compose effectها (LaunchedEffect, SideEffect, DisposableEffect) به طور کامل جایگزین swizzling برای وظایف UI میشوند. آنها روشی اعلامی، قابل پیشبینی و قابل تست برای افزودن رفتار مقطعی بدون تغییر dispatch table ارائه میدهند. در پروژههای جدید، Apple و Google دقیقاً این رویکرد را به جای رهگیری runtime توصیه میکنند.
سوالات متداول
Method Swizzling در صورت رعایت قوانین برای production قابل قبول است: dispatch_once برای اجرای یکباره، فراخوانی پیادهسازی اصلی، تست در تمام نسخههای iOS و مستندسازی. برای کارهای ساده بهتر است از delegateها یا زیرکلاسسازی استفاده کنید. Swizzling در production برای کتابخانههای مانیتورینگ و آنالیتیکس موجه است.
Method Swizzling — تکنیک خاص جایگزینی IMP در dispatch table است. AOP (برنامهنویسی جنبهگرا) — پارادایمی است که در آن swizzling میتواند به عنوان یکی از مکانیسمها استفاده شود. AOP همچنین شامل compile-time weaving (AspectJ)، رهگیری مبتنی بر proxy (Spring AOP) و code generation است.
از breakpoint در objc_msgSend برای ردیابی تمام پیامها استفاده کنید. یک breakpoint نمادین روی method_exchangeImplementations با شرط روی نام کلاس اضافه کنید. ابزار FLEX نشان میدهد که کدام متدهای کلاس swizzle شدهاند. برای بررسی سیستماتیک از اسکریپت lldb استفاده کنید که dispatch table کلاس را نمایش میدهد.
Swift از swizzling در سطح زبان پشتیبانی نمیکند. Method Swizzling فقط برای متدهایی که با @objc dynamic علامتگذاری شدهاند و از طریق Objective-C Runtime کامپایل میشوند کار میکند. متدهای خالص Swift (بدون @objc) از توزیع ایستا استفاده میکنند و نمیتوانند swizzle شوند — dispatch table آنها برای تغییر در دسترس نیست.
Firebase Analytics (swizzling viewDidAppear برای ردیابی خودکار صفحه)، Amplitude، Mixpanel، FLEX (بازرسی UI)، OHHTTPStubs (mock کردن درخواستهای شبکه)، Aspects (چارچوب AOP). همه آنها swizzling را در +load از طریق dispatch_once با فراخوانی پیادهسازی اصلی انجام میدهند.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید