ฉันคิดว่านี่เป็นคำถามที่ยอดเยี่ยมจริงๆและมันเป็นความอัปยศที่แทนที่จะจัดการกับคำถามจริง ๆ คำตอบส่วนใหญ่ทำให้ปัญหาเกิดขึ้น
การใช้วิธีการที่ร้อนจัดก็เหมือนกับการใช้มีดคม ๆ ในห้องครัว บางคนกลัวของมีดคมเพราะพวกเขาคิดว่าพวกเขาจะตัดตัวเองไม่ดี แต่ความจริงก็คือมีดที่คมชัดมีความปลอดภัย
การใช้วิธีการแบบ swizzling สามารถเขียนโค้ดได้ดีขึ้นมีประสิทธิภาพมากขึ้นและบำรุงรักษาได้มากขึ้น นอกจากนี้ยังสามารถถูกทารุณกรรมและนำไปสู่ข้อบกพร่องที่น่ากลัว
พื้นหลัง
เช่นเดียวกับรูปแบบการออกแบบทั้งหมดหากเราตระหนักถึงผลที่จะตามมาของรูปแบบนั้นอย่างเต็มที่เราสามารถตัดสินใจได้อย่างละเอียดมากขึ้นว่าจะใช้หรือไม่ Singletons เป็นตัวอย่างที่ดีของบางสิ่งบางอย่างที่ค่อนข้างขัดแย้งและด้วยเหตุผลที่ดี - มันยากที่จะนำไปใช้อย่างถูกต้อง หลายคนยังคงเลือกใช้ซิงเกิลตัน เช่นเดียวกันสามารถพูดเกี่ยวกับการว่องไว คุณควรแสดงความคิดเห็นของคุณเองเมื่อคุณเข้าใจทั้งดีและไม่ดี
อภิปรายผล
นี่คือบางส่วนของข้อผิดพลาดของวิธีการที่ว่องไว:
- วิธีการที่ว่องไวไม่ได้เป็นอะตอม
- เปลี่ยนพฤติกรรมของรหัสที่ไม่ได้เป็นเจ้าของ
- ข้อขัดแย้งในการตั้งชื่อที่เป็นไปได้
- Swizzling เปลี่ยนอาร์กิวเมนต์ของเมธอด
- คำสั่งของ Swizzles สำคัญ
- ยากที่จะเข้าใจ (ดูซ้ำ)
- การดีบักยาก
ประเด็นเหล่านี้ถูกต้องทั้งหมดและในการจัดการกับปัญหาเราสามารถปรับปรุงทั้งความเข้าใจของเราเกี่ยวกับวิธีการที่เกิดปัญหาและวิธีการที่ใช้เพื่อให้ได้ผลลัพธ์ ฉันจะกินทีละคน
วิธีการที่ว่องไวไม่ได้เป็นอะตอม
ฉันยังไม่เห็นการดำเนินการตามวิธี swizzling ที่มีความปลอดภัยที่จะใช้ควบคู่กันไป1 นี่ไม่ใช่ปัญหาจริงใน 95% ของกรณีที่คุณต้องการใช้วิธีการแบบ swizzling โดยปกติแล้วคุณเพียงแค่ต้องการแทนที่การใช้งานของวิธีการและคุณต้องการให้การใช้งานที่จะใช้ตลอดชีวิตของโปรแกรมของคุณ ซึ่งหมายความว่าคุณควรทำวิธีการของคุณร้อน+(void)load
แรง load
วิธีการเรียนจะถูกดำเนินการเป็นลำดับที่เริ่มต้นของแอพลิเคชันของคุณ คุณจะไม่มีปัญหาใด ๆ กับการเห็นพ้องด้วยถ้าคุณทำที่นี่ หากคุณต้องรีบ+(void)initialize
เสียงฟู่อย่างแรงคุณสามารถจบลงด้วยสภาพการแข่งขันในการติดตั้งแบบรวดเร็วและรันไทม์อาจสิ้นสุดลงในสภาวะที่แปลก
เปลี่ยนพฤติกรรมของรหัสที่ไม่ได้เป็นเจ้าของ
นี่เป็นปัญหาของการ swizzling แต่มันเป็นประเด็นทั้งหมด เป้าหมายคือสามารถเปลี่ยนรหัสนั้นได้ เหตุผลที่ผู้คนชี้ให้เห็นว่าเรื่องนี้เป็นเรื่องใหญ่เพราะคุณไม่เพียงแค่เปลี่ยนสิ่งต่าง ๆ สำหรับอินสแตนซ์เดียวNSButton
ที่คุณต้องการเปลี่ยนสิ่งต่าง ๆ แต่สำหรับทุกคนNSButton
กรณีในใบสมัครของคุณ ด้วยเหตุนี้คุณจึงควรระมัดระวังเมื่อคุณ swizzle แต่คุณไม่จำเป็นต้องหลีกเลี่ยงโดยสิ้นเชิง
ลองใช้วิธีนี้ ... ถ้าคุณแทนที่เมธอดในคลาสและคุณไม่เรียกเมธอดของคลาส super คุณอาจทำให้เกิดปัญหา ในกรณีส่วนใหญ่ผู้เรียนระดับสูงคาดว่าจะเรียกใช้วิธีการนั้นได้ (เว้นแต่จะมีการบันทึกไว้เป็นอย่างอื่น) หากคุณใช้ความคิดแบบเดียวกันนี้กับการมั่วสุมคุณได้ครอบคลุมปัญหาส่วนใหญ่ เรียกการใช้งานดั้งเดิมเสมอ หากคุณไม่ทำคุณอาจเปลี่ยนไปมากเกินไปเพื่อความปลอดภัย
ข้อขัดแย้งในการตั้งชื่อที่เป็นไปได้
ความขัดแย้งในการตั้งชื่อเป็นปัญหาทั้งหมดทั่วทั้งโกโก้ เรามักจะนำหน้าชื่อคลาสและชื่อเมธอดในหมวดหมู่ น่าเสียดายที่การตั้งชื่อความขัดแย้งเป็นภัยพิบัติในภาษาของเรา ในกรณีที่มีการว้าวุ่นพวกเขาไม่จำเป็นต้องเป็น เราเพียงแค่ต้องเปลี่ยนวิธีการที่เราคิดเกี่ยวกับวิธีการที่สั่นคลอนเล็กน้อย ส่วนใหญ่จะเป็นแบบนี้:
@interface NSView : NSObject
- (void)setFrame:(NSRect)frame;
@end
@implementation NSView (MyViewAdditions)
- (void)my_setFrame:(NSRect)frame {
// do custom work
[self my_setFrame:frame];
}
+ (void)load {
[self swizzle:@selector(setFrame:) with:@selector(my_setFrame:)];
}
@end
มันใช้งานได้ดี แต่จะเกิดอะไรขึ้นถ้าmy_setFrame:
ถูกกำหนดไว้ที่อื่น? ปัญหานี้ไม่ได้เกิดจากการ swizzling แต่เราสามารถแก้ไขได้ วิธีแก้ปัญหามีประโยชน์เพิ่มเติมของการแก้ไขข้อผิดพลาดอื่น ๆ เช่นกัน นี่คือสิ่งที่เราทำแทน:
@implementation NSView (MyViewAdditions)
static void MySetFrame(id self, SEL _cmd, NSRect frame);
static void (*SetFrameIMP)(id self, SEL _cmd, NSRect frame);
static void MySetFrame(id self, SEL _cmd, NSRect frame) {
// do custom work
SetFrameIMP(self, _cmd, frame);
}
+ (void)load {
[self swizzle:@selector(setFrame:) with:(IMP)MySetFrame store:(IMP *)&SetFrameIMP];
}
@end
แม้ว่าสิ่งนี้จะดูคล้ายกับ Objective-C เล็กน้อย (เนื่องจากใช้ตัวชี้ฟังก์ชั่น) แต่ก็หลีกเลี่ยงความขัดแย้งในการตั้งชื่อ โดยหลักการแล้วมันกำลังทำสิ่งเดียวกันกับมาตรฐานการ swizzling นี่อาจเป็นการเปลี่ยนแปลงเล็กน้อยสำหรับผู้ที่ใช้การ swizzling ตามที่กำหนดไว้ในขณะที่ แต่ในท้ายที่สุดฉันคิดว่ามันจะดีกว่า วิธีการ swizzling ถูกกำหนดไว้ดังนี้:
typedef IMP *IMPPointer;
BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) {
IMP imp = NULL;
Method method = class_getInstanceMethod(class, original);
if (method) {
const char *type = method_getTypeEncoding(method);
imp = class_replaceMethod(class, original, replacement, type);
if (!imp) {
imp = method_getImplementation(method);
}
}
if (imp && store) { *store = imp; }
return (imp != NULL);
}
@implementation NSObject (FRRuntimeAdditions)
+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {
return class_swizzleMethodAndStore(self, original, replacement, store);
}
@end
Swizzling โดยการเปลี่ยนชื่อเมธอดจะเปลี่ยนอาร์กิวเมนต์ของเมธอด
นี่คือสิ่งที่ยิ่งใหญ่ในใจของฉัน นี่คือเหตุผลที่ไม่ควรทำวิธีการมาตรฐานแบบ swizzling คุณกำลังเปลี่ยนข้อโต้แย้งที่ส่งผ่านไปยังการใช้วิธีการเดิม นี่คือที่มันเกิดขึ้น:
[self my_setFrame:frame];
บรรทัดนี้ทำอะไร:
objc_msgSend(self, @selector(my_setFrame:), frame);
my_setFrame:
ซึ่งจะใช้รันไทม์ที่จะมองขึ้นการดำเนินงานของ เมื่อพบว่ามีการนำไปใช้งานจะเรียกใช้งานด้วยอาร์กิวเมนต์เดียวกันที่ได้รับ การนำไปปฏิบัติที่พบนั้นเป็นการนำไปปฏิบัติดั้งเดิมsetFrame:
ดังนั้นจึงดำเนินการต่อไปและเรียกสิ่งนั้น แต่_cmd
ข้อโต้แย้งไม่setFrame:
เหมือนที่ควรจะเป็น my_setFrame:
ก็ตอนนี้ มีการเรียกใช้การเริ่มต้นด้วยอาร์กิวเมนต์โดยไม่คาดคิดว่าจะได้รับ มันไม่ดีเลย
มีวิธีแก้ปัญหาง่าย ๆ - ใช้เทคนิคทางเลือกใหม่ ๆ ที่กำหนดไว้ข้างต้น ข้อโต้แย้งจะยังคงไม่เปลี่ยนแปลง!
คำสั่งของ Swizzles สำคัญ
ลำดับที่วิธีการจะทำให้เกิดปัญหามากขึ้น สมมติว่าsetFrame:
มีการกำหนดไว้เท่านั้นNSView
ลองจินตนาการถึงสิ่งต่อไปนี้:
[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
จะเกิดอะไรขึ้นเมื่อวิธีการNSButton
เป็นแบบ swizzled? การเปลี่ยนแปลงที่รวดเร็วส่วนใหญ่จะช่วยให้มั่นใจได้ว่าจะไม่แทนที่การใช้งานของsetFrame:
ทุกมุมมองดังนั้นจึงจะดึงวิธีการอินสแตนซ์ สิ่งนี้จะใช้การนำไปปฏิบัติที่มีอยู่เพื่อกำหนดใหม่setFrame:
ในNSButton
คลาสเพื่อให้การแลกเปลี่ยนการนำไปใช้งานไม่ส่งผลกระทบต่อมุมมองทั้งหมด NSView
การดำเนินงานที่มีอยู่คนหนึ่งที่กำหนดไว้ใน สิ่งเดียวกันจะเกิดขึ้นเมื่อเปิดเครื่องNSControl
(ใช้งานอีกครั้งNSView
)
เมื่อคุณเรียกsetFrame:
ปุ่มมันจึงจะเรียกวิธี swizzled ของคุณแล้วกระโดดตรงไปที่setFrame:
วิธีการที่กำหนดไว้ NSView
แต่เดิม การใช้งานNSControl
และNSView
swizzled จะไม่ถูกเรียก
แต่ถ้าสั่งคือ:
[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
เนื่องจากการดูแบบ swizzling เกิดขึ้นก่อนการควบคุมแบบ swizzling จะสามารถดึงวิธีการที่เหมาะสม ในทำนองเดียวกันตั้งแต่ swizzling ควบคุมคือก่อน swizzling ปุ่มปุ่มจะดึงการดำเนิน swizzled setFrame:
การควบคุมของ นี่เป็นความสับสนเล็กน้อย แต่นี่เป็นลำดับที่ถูกต้อง เราจะมั่นใจในสิ่งนี้ได้อย่างไร
อีกครั้งเพียงใช้load
เพื่อทำสิ่งต่างๆ หากคุณ Swizzle เข้าload
และทำการเปลี่ยนแปลงเฉพาะคลาสที่กำลังโหลดคุณจะปลอดภัย load
วิธีการค้ำประกันว่าวิธีการโหลดชั้นซุปเปอร์จะถูกเรียกว่าก่อนที่จะ subclasses ใด ๆ เราจะได้คำสั่งที่ถูกต้องแน่นอน!
ยากที่จะเข้าใจ (ดูซ้ำ)
เมื่อมองไปที่วิธีการแบบ swizzled ที่กำหนดไว้ตามเนื้อผ้าฉันคิดว่ามันยากที่จะบอกว่าเกิดอะไรขึ้น แต่เมื่อมองไปที่วิธีอื่นที่เราทำไว้แล้วเราก็เข้าใจง่าย คนนี้ถูกแก้ไขแล้ว!
การดีบักยาก
หนึ่งในความสับสนระหว่างการดีบั๊กคือการเห็น backtrace แปลก ๆ ที่ชื่อ swizzled นั้นปะปนกันและทุกสิ่งทุกอย่างก็สับสนไปในหัวของคุณ อีกครั้งการดำเนินการทางเลือกที่อยู่นี้ คุณจะเห็นฟังก์ชั่นตั้งชื่ออย่างชัดเจนใน backtraces ถึงกระนั้นการดีบั๊กอาจทำได้ยากเพราะมันยากที่จะจำได้ว่าอะไรที่ส่งผลกระทบกับการมั่ว จัดทำเอกสารรหัสของคุณได้ดี (แม้ว่าคุณคิดว่าคุณเป็นผู้เดียวเท่านั้นที่จะเห็นมัน) ปฏิบัติตามแนวทางปฏิบัติที่ดีและคุณจะไม่เป็นไร การดีบักไม่ยากกว่ารหัสแบบมัลติเธรด
ข้อสรุป
วิธีการที่รวดเร็วจะปลอดภัยหากใช้อย่างถูกต้อง วัดความปลอดภัยง่ายๆที่คุณสามารถใช้เพื่อ swizzle load
เฉพาะใน เช่นเดียวกับหลาย ๆ อย่างในการเขียนโปรแกรมอาจเป็นอันตราย แต่การเข้าใจผลที่ตามมาจะช่วยให้คุณใช้งานได้อย่างถูกต้อง
1โดยใช้วิธีการ swizzling ที่กำหนดไว้ข้างต้นคุณสามารถทำให้สิ่งต่างๆปลอดภัยได้ถ้าคุณใช้ trampolines คุณจะต้องมีสองแทรมโพลีน ที่จุดเริ่มต้นของวิธีการคุณจะต้องกำหนดตัวชี้ฟังก์ชั่นstore
ให้กับฟังก์ชั่นที่หมุนจนกว่าที่อยู่ที่store
ชี้ไปจะเปลี่ยน สิ่งนี้จะหลีกเลี่ยงสภาวะการแย่งชิงที่เรียกใช้วิธีการแบบ swizzled ก่อนที่คุณจะสามารถตั้งค่าstore
ตัวชี้ฟังก์ชันได้ จากนั้นคุณจะต้องใช้แทรมโพลีนในกรณีที่การใช้งานไม่ได้กำหนดไว้ในชั้นเรียนและมีการค้นหาแทรมโพลีนและเรียกใช้วิธีซูเปอร์คลาสอย่างถูกต้อง การกำหนดวิธีการเพื่อให้สามารถค้นหาการใช้งานขั้นสูงแบบไดนามิกได้จะทำให้มั่นใจได้ว่าลำดับการโทรแบบ swizzling ไม่สำคัญ