Method Swizzling هي تقنية وقت تشغيل يتم فيها تبادل تطبيقات اثنين من طرق الفئة أثناء التنفيذ. تتيح تجاوز أو استكمال سلوك طريقة نظام دون إنشاء فئة فرعية أو تعديل الكود المصدري. وجدت هذه التقنية أكبر تطبيق لها في تطوير iOS باستخدام Objective-C، لكن توجد نظائرها في Kotlin/Android عبر الانعكاس. وفقاً لدليل NSHipster بواسطة Mattt، 2024، فإن swizzling من أقوى وأخطر آليات Objective-C Runtime.
الملخص
Method Swizzling هي تقنية وقت تشغيل تتبادل تطبيقات اثنين من طرق Objective-C. بعد swizzling، استدعاء originalSelector ينفذ كود swizzledSelector، والعكس صحيح. هذا ممكن بفضل بنية Objective-C Runtime، حيث كل محدد (SEL) مرتبط بتطبيق (IMP) عبر جدول إرسال — جدول يمكن تعديله أثناء التنفيذ.
مصطلح «swizzling» تم تقديمه في مجتمع مطوري Cocoa في أوائل العقد الأول من القرن الحادي والعشرين. اكتسبت التقنية شهرة واسعة بفضل مكتبات مثل: AFNetworking (swizzling UIWebView لتتبع التحميل)، Aspects (إطار AOP قائم على swizzling) وFLEX (أداة تصحيح تقوم بتبديل طرق النظام للتفتيش). اليوم، يُستخدم swizzling ضمنياً في معظم تطبيقات iOS — من خلال مكتبات المراقبة والتحليلات.
خاصية مهمة للswizzling هي العالمية: استبدال التطبيق يحدث على مستوى الفئة، وليس على مستوى المثيل. إذا قامت مكتبة بتبديل طريقة UIViewController.viewDidLoad، فإن ذلك يؤثر على جميع مثيلات UIViewController في التطبيق، بما في ذلك مثيلات النظام. هذه هي قوة swizzling — سطر واحد من الكود يغير سلوك التطبيق بأكمله — والمصدر الرئيسي للأخطاء.
Objective-C Runtime يخزن في كل فئة جدول إرسال — قاموس حيث المفتاح هو 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). تقوم بتبادل IMPs لكائني Method بشكل ذري. بعد الاستدعاء، يتم تعديل جدول إرسال الفئة: استدعاء original ينفذ كود swizzled، واستدعاء swizzled ينفذ كود original. تضيف فئة SafeSwizzle هذه الطريقة إلى جميع NSObject، مما يسمح لأي فئة بإجراء swizzling.
يتطلب التنفيذ الآمن للswizzling استدعاء التنفيذ الأصلي داخل النسخة المستبدلة. وإلا، يُفقد السلوك الأصلي للطريقة بشكل نهائي. النمط الصحيح هو حفظ IMP الأصلي قبل التبادل واستدعائه في الطريقة المستبدلة:
// 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 لنفس الطريقة سيؤدي إلى تكرار لا نهائي: الطريقة المستبدلة ستستدعي نفسها. +load يتم استدعاؤه عند تحميل الفئة في وقت التشغيل — إنها نقطة آمنة للswizzling يتم تنفيذها قبل كود التطبيق الرئيسي.
جدول الإرسال لفئة Objective-C هو مصفوفة من بنى method_t تحتوي على SEL وIMP ونوع القيمة المُعادة. method_exchangeImplementations ببساطة تتبادل مؤشري IMP في هذا الجدول. مهم: swizzling يعمل فقط على مستوى الفئة، وليس على مستوى البروتوكول. إذا تم تعريف طريقة في بروتوكول ولكن لم يتم تنفيذها، فإن جدول الإرسال لا يحتوي على إدخال للswizzling.
تكلفة swizzling ضئيلة — تبادل مؤشري IMP في جدول الإرسال يستغرق بضع نانوثوان. بعد swizzling، لا تبطئ استدعاءات الطريقة: objc_msgSend يجد IMP في نفس وقت O(1) كما قبل swizzling. العملية الإضافية الوحيدة هي التحقق من ذاكرة التخزين المؤقت للطرق عند أول استدعاء بعد التبادل. وفقاً لبيانات فريق أداء Apple، لا يؤثر swizzling على أداء التطبيق.
Method Swizzling يُستخدم في ثلاثة سيناريوهات رئيسية: المراقبة والتحليلات (تتبع viewDidLoad وviewDidAppear للإرسال التلقائي للأحداث)، اعتراض AOP (تسجيل معلمات جميع استدعاءات الطرق)، والإصلاح السريع (إصلاح خطأ في الإنتاج دون مراجعة App Store عبر مكتبات مثل JSPatch).
كل من هذه السيناريوهات يعمل لأن swizzling يُطبق مركزياً. مكتبة تحليلات تقوم بـ swizzling مرة واحدة في +load، وتبدأ جميع مثيلات UIViewController في التطبيق بإرسال الأحداث. لا يحتاج المطور إلى إضافة كود في كل وحدة تحكم — هذا يقلل التكرار وخطر الأخطاء.
على Android، swizzling الطرق بالمعنى الكلاسيكي لـ Objective-C مستحيل — Java/Kotlin تستخدم الإرسال الثابت عبر vtable. ومع ذلك، توجد آليات تحقق تأثيراً مشابهاً: Java Reflection لاستبدال التنفيذ في وقت التشغيل وGradle Transform API / ASM لتعديل البايت كود في وقت البناء.
// Swizzling على Android عبر الانعكاس + companion object
class Logger {
companion object {
var originalImpl: (() -> Unit)? = null
}
fun log() {
println("السجل الأصلي")
}
}
// استبدال التنفيذ في وقت التشغيل عبر الانعكاس
fun swizzleLog() {
val originalMethod = Logger::class.java
.getDeclaredMethod("log")
originalMethod.isAccessible = true
Logger.originalImpl = {
originalMethod.invoke(Logger())
}
// الاستبدال عبر دالة مضمنة
println("swizzled: تم اعتراض السجل")
}
هذا الكود يستبدل سلوك طريقة log() عبر Java Reflection: getDeclaredMethod يصل إلى التنفيذ الخاص، isAccessible يعطل فحوصات الوصول. بدلاً من استدعاء log() مباشرة، يتم استدعاء غلاف ينفذ منطقاً إضافياً. ومع ذلك، يقوم Android بتحسين الطرق الساخنة عبر JIT — قد لا يعمل الانعكاس على أجزاء AOT المترجمة بالفعل.
نهج أكثر موثوقية هو معالجة البايت كود عبر Gradle Transform API أو AGP (Android Gradle Plugin) مع مكتبة ASM. يتم تعديل البايت كود في وقت الترجمة: ASM يضيف استدعاءات إلى كل طريقة في الفئة. هكذا تعمل أدوات تغطية الكود (JaCoCo) ومراقبة الأداء (Firebase Performance Monitoring).
Method Swizzling هي تقنية عالية المخاطر. تعارض بين المكتبات: إذا قامت مكتبتان بتبديل نفس الطريقة، فإن ترتيب التنفيذ غير مضمون. عدم التوافق مع تحديثات iOS: إذا غيرت Apple التوقيع أو أزالت طريقة في إصدار iOS جديد، يؤدي swizzling إلى تعطل. عدم الرؤية في الكود: swizzling غير مرئي في تنفيذ الفئة، مما يعقد التصحيح.
| الخطر | الوصف | التخفيف |
|---|---|---|
| تعارض المكتبات | مكتبتان تتبادلان viewDidAppear — إحداهما تعطل الأخرى | التحقق مما إذا كانت الطريقة قد تم تبديلها بالفعل عبر class_getInstanceMethod |
| التكرار | إعادة swizzling لنفس الطريقة يسبب حلقة لا نهائية | استخدام dispatch_once دائماً |
| تغيير التوقيع | Apple تغير توقيع الطريقة في إصدار iOS جديد — عدم تطابق IMP | الاختبار على جميع إصدارات iOS المدعومة |
| عدم الرؤية | Swizzling لا يظهر في مكدس استدعاءات Xcode | توثيق جميع عمليات swizzling في الكود |
| مراجعة App Store | Apple ترفض التطبيقات ذات swizzling غير الموثق | استخدام APIs العامة فقط وتوثيق الغرض |
أفضل الممارسات للswizzling الآمن تشمل: استدعاء التنفيذ الأصلي دائماً، إجراء swizzling بدقة في +load عبر dispatch_once، تسمية الطرق المستبدلة ببادئة (مثل s_originalMethodName)، توثيق كل عملية swizzling بهدفها. مكتبة Aspects تحل مشكلة التعارض من خلال التنفيذ المتسلسل للكتل قبل/بعد الطريقة الأصلية.
بدائل method swizzling مفضلة لكود الإنتاج بسبب القدرة على التنبؤ والأمان. المفوضون والبروتوكولات (UIApplicationDelegate, UITableViewDelegate) يقدمون نقاط توسع صريحة دون تعديل وقت التشغيل. إنشاء الفئات الفرعية — إنشاء فئة فرعية من UIViewController مع تجاوز viewDidAppear — يعمل بشكل متوقع وليس له تعارضات.
SwiftUI وCombine يلغي الحاجة إلى swizzling: المعدلات (onAppear, onChange) تضيف السلوك بشكل تصريحي، دون تجاوز الطرق. في Android Jetpack Compose يحقق نفس الشيء من خلال التأثيرات (LaunchedEffect, SideEffect) والمعدلات. أطر AOP (AspectJ لنظام Android، InterposeKit لنظام iOS) توفر بديلاً آمناً مع weaving في وقت الترجمة.
وفقاً لبيانات Apple WWDC 2024، فإن وقت تشغيل Swift لا يدعم method swizzling على مستوى اللغة — طرق @objc dynamic يمكن تبديلها فقط من خلال Objective-C Runtime. تطبيقات Swift التي لا تستخدم @objc محمية تماماً من swizzling العرضي بواسطة مكتبات الطرف الثالث. هذا يجعل Swift أكثر أماناً ولكنه يحد من قدرات التجهيز في وقت التشغيل.
معدلات SwiftUI (onAppear, onChange, onReceive) وتأثيرات Jetpack Compose (LaunchedEffect, SideEffect, DisposableEffect) تحل محل swizzling بالكامل لمهام واجهة المستخدم. توفر طريقة تصريحية ومتوقعة وقابلة للاختبار لإضافة سلوك شامل دون تعديل جدول الإرسال. في المشاريع الجديدة، توصي Apple وGoogle بهذا النهج بدلاً من الاعتراض في وقت التشغيل.
الأسئلة الشائعة
Method Swizzling مقبول للإنتاج عند اتباع القواعد: dispatch_once للتنفيذ لمرة واحدة، استدعاء التنفيذ الأصلي، الاختبار على جميع إصدارات iOS والتوثيق. للمهام البسيطة، من الأفضل استخدام المفوضين أو إنشاء الفئات الفرعية. Swizzling في الإنتاج مبرر لمكتبات المراقبة والتحليلات.
Method Swizzling هي تقنية محددة لاستبدال IMP في جدول الإرسال. AOP (البرمجة الموجهة بالجوانب) هو نموذج يمكن أن يُستخدم فيه swizzling كإحدى الآليات. يشمل AOP أيضاً weaving وقت الترجمة (AspectJ)، الاعتراض القائم على الوكيل (Spring AOP) وتوليد الكود.
استخدام نقطة توقف في objc_msgSend لتتبع جميع الرسائل. إضافة نقطة توقف رمزية على method_exchangeImplementations بشرط على اسم الفئة. أداة FLEX تظهر أي طرق الفئة تم تبديلها. للتحقق المنتظم، استخدام برنامج lldb النصي الذي يُخرج جدول إرسال الفئة.
Swift لا يدعم swizzling على مستوى اللغة. Method Swizzling يعمل فقط للطرق الموسومة بـ @objc dynamic، التي يتم ترجمتها عبر Objective-C Runtime. طرق Swift النقية (بدون @objc) تستخدم الإرسال الثابت ولا يمكن تبديلها — جدول الإرسال الخاص بها غير قابل للتعديل.
Firebase Analytics (swizzling viewDidAppear للتتبع التلقائي للشاشات)، Amplitude، Mixpanel، FLEX (تفتيش واجهة المستخدم)، OHHTTPStubs (محاكاة طلبات الشبكة)، Aspects (إطار AOP). جميعها تقوم بـ swizzling في +load عبر dispatch_once مع استدعاء التنفيذ الأصلي.
الخلاصة
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا