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) เชื่อมโยงกับ implementation (IMP) ผ่านตารางการจัดส่ง (dispatch table) — ตารางที่สามารถแก้ไขได้ระหว่างการทำงาน
คำว่า “swizzling” ถูกนำมาใช้ในชุมชนนักพัฒนา Cocoa ในช่วงต้นทศวรรษ 2000 เทคนิคนี้เป็นที่รู้จักอย่างกว้างขวางต้องขอบคุณไลบรารีเช่น: AFNetworking (การ swizzling UIWebView เพื่อติดตามการโหลด), Aspects (เฟรมเวิร์ก AOP ที่ใช้ swizzling) และ FLEX (เครื่องมือดีบักที่ swizzle เมธอดระบบเพื่อตรวจสอบ) ปัจจุบัน swizzling ถูกใช้โดยปริยายในแอปพลิเคชัน iOS ส่วนใหญ่ — ผ่านไลบรารีการตรวจสอบและวิเคราะห์
คุณสมบัติที่สำคัญของ swizzling คือ ความเป็นสากล: การแทนที่ implementation เกิดขึ้นที่ระดับคลาส ไม่ใช่ระดับอินสแตนซ์ หากไลบรารี swizzle เมธอด UIViewController.viewDidLoad มันจะส่งผลกระทบต่ออินสแตนซ์ UIViewController ทั้งหมดในแอปพลิเคชัน รวมถึงของระบบด้วย นี่เป็นทั้งจุดแข็งของ swizzling — โค้ดหนึ่งบรรทัดเปลี่ยนพฤติกรรมของทั้งแอปพลิเคชัน — และเป็นแหล่งที่มาหลักของบั๊ก
Objective-C Runtime เก็บตารางการจัดส่งในแต่ละคลาส — พจนานุกรมที่คีย์คือ SEL (ตัวระบุเมธอด) และค่าคือ IMP (ตัวชี้ไปยังฟังก์ชัน implementation) เมื่อแอปพลิเคชันส่งข้อความไปยังออบเจกต์ 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 ที่ปลอดภัยต้องเรียก implementation ดั้งเดิม ภายในเวอร์ชันที่ถูก swizzle มิฉะนั้น พฤติกรรมดั้งเดิมของเมธอดจะสูญหายไปอย่างถาวร รูปแบบที่ถูกต้องคือบันทึก IMP ดั้งเดิมไว้ก่อนการสลับและเรียกมันในเมธอดที่ถูก swizzle:
// Swizzling พร้อมการเรียก implementation ดั้งเดิม
- (void)swizzled_viewDidLoad {
// 1. การเรียก implementation ดั้งเดิม
[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 ซ้ำเมธอดเดียวกันจะทำให้เกิดการเรียกซ้ำไม่มีที่สิ้นสุด: เมธอดที่ถูก swizzle จะเรียกตัวเอง +load ถูกเรียกเมื่อคลาสถูกโหลดเข้าสู่ runtime — มันเป็นจุดที่ปลอดภัยสำหรับ swizzling ที่ทำงานก่อนโค้ดหลักของแอปพลิเคชัน
ตารางการจัดส่งของคลาส Objective-C คืออาร์เรย์ของโครงสร้าง method_t ที่ประกอบด้วย SEL, IMP และชนิดค่าที่ส่งคืน method_exchangeImplementations เพียงแค่สลับตัวชี้ IMP สองตัวในตารางนี้ สำคัญ: swizzling ทำงานเฉพาะที่ระดับคลาส ไม่ใช่ระดับโปรโตคอล หากเมธอดถูกกำหนดในโปรโตคอลแต่ไม่ได้ถูกนำไปใช้ ตารางการจัดส่งจะไม่มีรายการสำหรับ swizzling
ค่าใช้จ่ายของ swizzling น้อยมาก — การสลับตัวชี้ IMP สองตัวในตารางการจัดส่งใช้เวลาเพียงไม่กี่นาโนวินาที หลังจาก swizzling การจัดส่งเมธอดจะไม่ช้าลง: objc_msgSend ค้นหา IMP ในเวลา O(1) เดียวกับก่อน swizzling การดำเนินการเพิ่มเติมเพียงอย่างเดียวคือการตรวจสอบแคชเมธอดในการเรียกครั้งแรกหลังการสลับ ตามข้อมูลของทีม Apple Performance, swizzling ไม่ส่งผลกระทบต่อประสิทธิภาพของแอปพลิเคชัน
Method Swizzling ถูกใช้ในสามสถานการณ์หลัก: การตรวจสอบและวิเคราะห์ (การติดตาม viewDidLoad, viewDidAppear สำหรับการส่งเหตุการณ์อัตโนมัติ), การสกัดกั้น AOP (การบันทึกพารามิเตอร์ของการเรียกเมธอดทั้งหมด), และ hotfix (การแก้ไขบั๊กในโปรดักชันโดยไม่ต้องผ่าน App Store Review ผ่านไลบรารีเช่น JSPatch)
แต่ละสถานการณ์เหล่านี้ทำงานได้เพราะ swizzling ถูกนำไปใช้ แบบรวมศูนย์ ไลบรารีวิเคราะห์ทำ swizzling หนึ่งครั้งใน +load และอินสแตนซ์ UIViewController ทั้งหมดในแอปพลิเคชันเริ่มส่งเหตุการณ์ นักพัฒนาไม่จำเป็นต้องเพิ่มโค้ดในทุกคอนโทรลเลอร์ — ซึ่งช่วยลดการทำซ้ำและความเสี่ยงของข้อผิดพลาด
บน Android method swizzling ในความหมายดั้งเดิมของ Objective-C เป็นไปไม่ได้ — Java/Kotlin ใช้การจัดส่งแบบสแตติกผ่าน vtable อย่างไรก็ตาม มีกลไกที่ให้ผลลัพธ์คล้ายกัน: Java Reflection สำหรับการแทนที่ implementation ใน runtime และ Gradle Transform API / ASM สำหรับการปรับเปลี่ยนไบต์โค้ดในเวลาสร้าง
// Swizzling บน Android ผ่านการสะท้อน + companion object
class Logger {
companion object {
var originalImpl: (() -> Unit)? = null
}
fun log() {
println("บันทึกดั้งเดิม")
}
}
// การแทนที่ implementation ใน runtime ผ่านการสะท้อน
fun swizzleLog() {
val originalMethod = Logger::class.java
.getDeclaredMethod("log")
originalMethod.isAccessible = true
Logger.originalImpl = {
originalMethod.invoke(Logger())
}
// การแทนที่ผ่านฟังก์ชันอินไลน์
println("swizzled: บันทึกถูกสกัดกั้น")
}
โค้ดนี้แทนที่พฤติกรรมของเมธอด log() ผ่าน Java Reflection: getDeclaredMethod เข้าถึง implementation ส่วนตัว, isAccessible ปิดการตรวจสอบการเข้าถึง แทนที่จะเรียก log() โดยตรง จะมีการเรียก wrapper ที่ดำเนินการตรรกะเพิ่มเติม อย่างไรก็ตาม Android ปรับปรุงประสิทธิภาพเมธอดที่ร้อนผ่าน JIT — การสะท้อนอาจไม่ทำงานบนเซ็กเมนต์ AOT ที่ถูกคอมไพล์แล้ว
วิธีการที่เชื่อถือได้มากขึ้นคือ การจัดการไบต์โค้ด ผ่าน Gradle Transform API หรือ AGP (Android Gradle Plugin) กับไลบรารี ASM การปรับเปลี่ยนไบต์โค้ดจะดำเนินการในเวลาคอมไพล์: ASM เพิ่มการเรียกไปยังทุกเมธอดของคลาส นี่คือวิธีการทำงานของเครื่องมือครอบคลุมโค้ด (JaCoCo) และเครื่องมือตรวจสอบประสิทธิภาพ (Firebase Performance Monitoring)
Method Swizzling เป็นเทคนิคที่มีความเสี่ยงสูง ความขัดแย้งระหว่างไลบรารี: หากสองไลบรารี swizzle เมธอดเดียวกัน ลำดับการทำงานไม่สามารถรับประกันได้ ความไม่เข้ากันกับการอัปเดต iOS: หาก Apple เปลี่ยนลายเซ็นหรือลบเมธอดในเวอร์ชัน iOS ใหม่ swizzling จะทำให้เกิดการล่ม ขาดการมองเห็นในโค้ด: swizzling ไม่ปรากฏในการทำงานของคลาส ทำให้การดีบักซับซ้อน
| ความเสี่ยง | คำอธิบาย | การลดความเสี่ยง |
|---|---|---|
| ความขัดแย้งของไลบรารี | ไลบรารีสองตัว swizzle viewDidAppear — ตัวหนึ่งทำให้อีกตัวเสีย | ตรวจสอบว่าเมธอดถูก swizzle แล้วหรือไม่ผ่าน class_getInstanceMethod |
| การเรียกซ้ำ | การ swizzling ซ้ำเมธอดเดียวกันทำให้เกิดลูปไม่มีที่สิ้นสุด | ใช้ dispatch_once เสมอ |
| การเปลี่ยนแปลงลายเซ็น | Apple เปลี่ยนลายเซ็นเมธอดใน iOS ใหม่ — IMP ไม่ตรงกัน | ทดสอบบน iOS ทุกเวอร์ชันที่รองรับ |
| การมองไม่เห็น | Swizzling ไม่ปรากฏในสแต็กการเรียกของ Xcode | บันทึกการดำเนินการ swizzling ทั้งหมดในโค้ด |
| App Review | Apple ปฏิเสธแอปพลิเคชันที่มี swizzling ที่ไม่ได้บันทึก | ใช้เฉพาะ API สาธารณะและบันทึกวัตถุประสงค์ |
แนวปฏิบัติที่ดีที่สุด สำหรับ swizzling ที่ปลอดภัยรวมถึง: เรียก implementation ดั้งเดิมเสมอ, ทำ swizzling ใน +load ผ่าน dispatch_once อย่างเคร่งครัด, ตั้งชื่อเมธอดที่ถูก swizzle ด้วยคำนำหน้า (เช่น s_originalMethodName), บันทึกการดำเนินการ swizzling แต่ละครั้งพร้อมวัตถุประสงค์ ไลบรารี Aspects แก้ปัญหาความขัดแย้งผ่านการดำเนินการแบบลูกโซ่ของบล็อกก่อน/หลังเมธอดดั้งเดิม
ทางเลือกแทน method swizzling เป็นที่นิยมสำหรับโค้ดโปรดักชันเนื่องจากความสามารถในการคาดการณ์และความปลอดภัย ตัวแทนและโปรโตคอล (UIApplicationDelegate, UITableViewDelegate) ให้จุดขยายที่ชัดเจนโดยไม่ต้องปรับเปลี่ยน runtime การสร้างคลาสย่อย — การสร้างคลาสย่อยของ UIViewController ที่แทนที่ viewDidAppear — ทำงานได้อย่างคาดการณ์ได้และไม่มีความขัดแย้ง
SwiftUI และ Combine ขจัดความจำเป็นของ swizzling: ตัวปรับแต่ง (onAppear, onChange) เพิ่มพฤติกรรมแบบประกาศ โดยไม่ต้องแทนที่เมธอด ใน Android Jetpack Compose บรรลุผลเดียวกันผ่านเอฟเฟกต์ (LaunchedEffect, SideEffect) และตัวปรับแต่ง เฟรมเวิร์ก AOP (AspectJ สำหรับ Android, InterposeKit สำหรับ iOS) ให้ทางเลือกที่ปลอดภัยด้วยการทอในเวลาคอมไพล์
ตามข้อมูลจาก Apple WWDC 2024, Swift runtime ไม่รองรับ method swizzling ในระดับภาษา — เมธอด @objc dynamic สามารถถูก swizzle ได้ผ่าน Objective-C Runtime เท่านั้น แอปพลิเคชัน Swift ที่ไม่ใช้ @objc ได้รับการปกป้องอย่างสมบูรณ์จากการ swizzling โดยไม่ได้ตั้งใจโดยไลบรารีของบริษัทอื่น สิ่งนี้ทำให้ Swift ปลอดภัยยิ่งขึ้นแต่จำกัดความสามารถในการตรวจวัด runtime
ตัวปรับแต่งของ SwiftUI (onAppear, onChange, onReceive) และเอฟเฟกต์ของ Jetpack Compose (LaunchedEffect, SideEffect, DisposableEffect) แทนที่ swizzling อย่างสมบูรณ์สำหรับงาน UI พวกมันให้วิธีที่ประกาศ คาดการณ์ได้ และทดสอบได้ในการเพิ่มพฤติกรรมที่ตัดขวางโดยไม่ต้องปรับเปลี่ยนตารางการจัดส่ง ในโปรเจกต์ใหม่ Apple และ Google แนะนำวิธีนี้แทนการสกัดกั้นใน runtime
คำถามที่พบบ่อย
Method Swizzling ยอมรับได้สำหรับโปรดักชันเมื่อปฏิบัติตามกฎ: dispatch_once สำหรับการทำงานครั้งเดียว, การเรียก implementation ดั้งเดิม, การทดสอบบน iOS ทุกเวอร์ชัน และการบันทึกเอกสาร สำหรับงานง่าย ๆ ควรใช้ตัวแทนหรือการสร้างคลาสย่อย Swizzling ในโปรดักชัน มีความเหมาะสมสำหรับไลบรารีการตรวจสอบและวิเคราะห์
Method Swizzling เป็นเทคนิคเฉพาะสำหรับการแทนที่ IMP ในตารางการจัดส่ง AOP (การเขียนโปรแกรมเชิงลักษณะ) เป็นกระบวนทัศน์ที่ swizzling สามารถใช้เป็นหนึ่งในกลไก AOP ยังรวมถึงการทอในเวลาคอมไพล์ (AspectJ), การสกัดกั้นแบบพร็อกซี (Spring AOP) และการสร้างโค้ด
ใช้เบรกพอยต์ใน objc_msgSend เพื่อติดตามข้อความทั้งหมด เพิ่มเบรกพอยต์เชิงสัญลักษณ์บน method_exchangeImplementations โดยมีเงื่อนไขบนชื่อคลาส เครื่องมือ FLEX แสดงว่าเมธอดใดของคลาสถูก swizzle สำหรับการตรวจสอบอย่างเป็นระบบ ใช้สคริปต์ lldb ที่แสดงตารางการจัดส่งของคลาส
Swift ไม่รองรับ swizzling ในระดับภาษา Method Swizzling ทำงานเฉพาะกับเมธอดที่ถูกทำเครื่องหมาย @objc dynamic ซึ่งถูกคอมไพล์ผ่าน Objective-C Runtime เมธอด Swift บริสุทธิ์ (ไม่มี @objc) ใช้การจัดส่งแบบสแตติกและไม่สามารถถูก swizzle ได้ — ตารางการจัดส่งของพวกมันไม่สามารถเข้าถึงได้สำหรับการแก้ไข
Firebase Analytics (swizzling viewDidAppear สำหรับการติดตามหน้าจออัตโนมัติ), Amplitude, Mixpanel, FLEX (การตรวจสอบ UI), OHHTTPStubs (การจำลองคำขอเครือข่าย), Aspects (เฟรมเวิร์ก AOP) ทั้งหมดทำ swizzling ใน +load ผ่าน dispatch_once พร้อมการเรียก implementation ดั้งเดิม
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม