Tôi nghĩ rằng đây là một câu hỏi thực sự tuyệt vời, và thật xấu hổ thay vì giải quyết câu hỏi thực sự, hầu hết các câu trả lời đã bỏ qua vấn đề và nói đơn giản là không sử dụng phương pháp đánh bóng.
Sử dụng phương pháp xông khói cũng giống như dùng dao nhọn trong bếp. Một số người sợ dao sắc vì họ nghĩ rằng họ sẽ tự cắt mình rất tệ, nhưng sự thật là dao sắc sẽ an toàn hơn .
Phương pháp swizzling có thể được sử dụng để viết mã tốt hơn, hiệu quả hơn, dễ bảo trì hơn. Nó cũng có thể bị lạm dụng và dẫn đến những con bọ khủng khiếp.
Lý lịch
Như với tất cả các mẫu thiết kế, nếu chúng tôi nhận thức đầy đủ về hậu quả của mẫu, chúng tôi có thể đưa ra quyết định sáng suốt hơn về việc có nên sử dụng nó hay không. Singletons là một ví dụ điển hình cho điều gì đó gây tranh cãi và vì lý do chính đáng - chúng thực sự khó thực hiện đúng. Nhiều người vẫn chọn sử dụng singletons. Điều tương tự có thể được nói về swizzling. Bạn nên hình thành ý kiến của riêng mình một khi bạn hoàn toàn hiểu cả mặt tốt và mặt xấu.
Thảo luận
Dưới đây là một số cạm bẫy của phương pháp swizzling:
- Phương pháp swizzling không phải là nguyên tử
- Thay đổi hành vi của mã không sở hữu
- Xung đột đặt tên có thể
- Lướt thay đổi đối số của phương thức
- Thứ tự của các vấn đề
- Khó hiểu (có vẻ đệ quy)
- Khó gỡ lỗi
Những điểm này đều hợp lệ và khi giải quyết chúng, chúng ta có thể cải thiện cả sự hiểu biết của chúng ta về phương pháp thay đổi phương pháp cũng như phương pháp được sử dụng để đạt được kết quả. Tôi sẽ đưa từng người một.
Phương pháp swizzling không phải là nguyên tử
Tôi vẫn chưa thấy việc thực hiện phương pháp swizzling an toàn để sử dụng đồng thời 1 . Đây thực sự không phải là vấn đề trong 95% trường hợp bạn muốn sử dụng phương pháp swizzling. Thông thường, bạn chỉ muốn thay thế việc thực hiện một phương thức và bạn muốn việc triển khai đó được sử dụng trong toàn bộ thời gian của chương trình. Điều này có nghĩa là bạn nên thực hiện phương pháp của bạn +(void)load
. Các load
phương pháp lớp học được thực hiện nối tiếp vào lúc bắt đầu của ứng dụng. Bạn sẽ không có bất kỳ vấn đề nào với sự tương tranh nếu bạn thực hiện thao tác lộn xộn ở đây. +(void)initialize
Tuy nhiên, nếu bạn bị cuốn vào , bạn có thể gặp phải tình trạng chạy đua trong quá trình thực hiện chóng mặt và thời gian chạy có thể rơi vào trạng thái kỳ lạ.
Thay đổi hành vi của mã không sở hữu
Đây là một vấn đề với swizzling, nhưng nó là loại toàn bộ vấn đề. Mục tiêu là có thể thay đổi mã đó. Lý do mà mọi người chỉ ra đây là một vấn đề lớn là vì bạn không chỉ thay đổi mọi thứ cho một trường hợp NSButton
mà bạn muốn thay đổi mọi thứ mà thay vào đó là tất cả các NSButton
trường hợp trong ứng dụng của bạn. Vì lý do này, bạn nên thận trọng khi bạn thay đổi, nhưng bạn không cần phải tránh nó hoàn toàn.
Hãy nghĩ về nó theo cách này ... nếu bạn ghi đè một phương thức trong một lớp và bạn không gọi phương thức siêu hạng đó, bạn có thể gây ra vấn đề phát sinh. Trong hầu hết các trường hợp, siêu lớp đang mong đợi phương thức đó được gọi (trừ khi có tài liệu khác). Nếu bạn áp dụng cùng suy nghĩ này cho việc thay đổi, bạn đã giải quyết được hầu hết các vấn đề. Luôn luôn gọi thực hiện ban đầu. Nếu bạn không, có lẽ bạn đang thay đổi quá nhiều để an toàn.
Xung đột đặt tên có thể
Đặt tên xung đột là một vấn đề trong suốt ca cao. Chúng tôi thường xuyên tiền tố tên lớp và tên phương thức trong các thể loại. Thật không may, xung đột đặt tên là một bệnh dịch trong ngôn ngữ của chúng tôi. Tuy nhiên, trong trường hợp bị sưng, họ không cần phải như vậy. Chúng ta chỉ cần thay đổi cách chúng ta nghĩ về phương pháp phồng nhẹ. Hầu hết các vụ nổ được thực hiện như thế này:
@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
Điều này chỉ hoạt động tốt, nhưng điều gì sẽ xảy ra nếu my_setFrame:
được xác định ở một nơi khác? Vấn đề này không phải là duy nhất để gây xáo trộn, nhưng dù sao chúng ta cũng có thể giải quyết nó. Cách giải quyết có thêm một lợi ích của việc giải quyết các cạm bẫy khác. Đây là những gì chúng tôi làm thay thế:
@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
Mặc dù cái này trông hơi giống Objective-C (vì nó sử dụng các con trỏ hàm), nhưng nó tránh mọi xung đột đặt tên. Về nguyên tắc, nó hoạt động chính xác giống như độ xoáy tiêu chuẩn. Đây có thể là một chút thay đổi đối với những người đã sử dụng swizzling vì nó đã được xác định trong một thời gian, nhưng cuối cùng, tôi nghĩ rằng nó tốt hơn. Phương pháp swizzling được định nghĩa như vậy:
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
Lướt qua bằng cách đổi tên phương thức làm thay đổi đối số của phương thức
Đây là một trong những lớn trong tâm trí của tôi. Đây là lý do mà phương pháp tiêu chuẩn không nên được thực hiện. Bạn đang thay đổi các đối số được truyền cho việc triển khai phương thức ban đầu. Đây là nơi nó xảy ra:
[self my_setFrame:frame];
Những gì dòng này làm là:
objc_msgSend(self, @selector(my_setFrame:), frame);
Mà sẽ sử dụng thời gian chạy để tra cứu việc thực hiện my_setFrame:
. Sau khi thực hiện được tìm thấy, nó gọi thực hiện với cùng các đối số đã được đưa ra. Việc triển khai mà nó tìm thấy là việc thực hiện ban đầu setFrame:
, vì vậy nó đi trước và gọi điều đó, nhưng _cmd
đối số không setFrame:
giống như vậy. Bây giờ là bây giờ my_setFrame:
. Việc thực hiện ban đầu đang được gọi với một đối số mà nó không bao giờ mong đợi nó sẽ nhận được. Điều này là không tốt.
Có một giải pháp đơn giản - sử dụng kỹ thuật thay thế thay thế được xác định ở trên. Các đối số sẽ không thay đổi!
Thứ tự của các vấn đề
Thứ tự trong đó các phương pháp có được vấn đề swizzled. Giả địnhsetFrame:
chỉ được định nghĩa trên NSView
, hãy tưởng tượng thứ tự này:
[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
Điều gì xảy ra khi phương thức trên NSButton
bị xáo trộn? Hầu hết các sự thay đổi sẽ đảm bảo rằng nó không thay thế việc thực hiện setFrame:
cho tất cả các chế độ xem, vì vậy nó sẽ mở ra phương thức cá thể. Điều này sẽ sử dụng triển khai hiện có để xác định lại setFrame:
trongNSButton
lớp để việc thực hiện trao đổi không ảnh hưởng đến tất cả các khung nhìn. Việc thực hiện hiện tại là một trong những định nghĩa trên NSView
. Điều tương tự sẽ xảy ra khi bật lên NSControl
(một lần nữa sử dụng NSView
thực hiện).
Do đó, khi bạn gọi setFrame:
vào một nút, nó sẽ gọi phương thức bị xáo trộn của bạn, và sau đó nhảy thẳng đến setFrame:
phương thức được xác định ban đầu NSView
. Các triển khai NSControl
và NSView
swizzled sẽ không được gọi.
Nhưng nếu thứ tự là:
[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
Kể từ khi chế độ xem xáo trộn diễn ra trước tiên, điều khiển phồng sẽ có thể kéo lên đúng phương pháp. Tương tự như vậy, vì điều khiển bị phồng lên trước khi nút bị phồng lên, nút sẽ kéo lên việc thực hiện bị trượt của điều khiển setFrame:
. Điều này hơi khó hiểu, nhưng đây là thứ tự chính xác. Làm thế nào chúng ta có thể đảm bảo thứ tự này của mọi thứ?
Một lần nữa, chỉ cần sử dụng load
để làm xáo trộn mọi thứ. Nếu bạn lướt qua load
và bạn chỉ thực hiện thay đổi cho lớp đang được tải, bạn sẽ an toàn. Các load
đảm bảo phương pháp mà các siêu phương pháp nạp lớp sẽ được gọi trước khi bất kỳ lớp con. Chúng tôi sẽ nhận được thứ tự chính xác!
Khó hiểu (có vẻ đệ quy)
Nhìn vào một phương pháp xáo trộn được xác định theo truyền thống, tôi nghĩ thật khó để biết chuyện gì đang xảy ra. Nhưng nhìn vào cách khác mà chúng ta đã thực hiện ở trên, điều đó thật dễ hiểu. Điều này đã được giải quyết!
Khó gỡ lỗi
Một trong những nhầm lẫn trong quá trình gỡ lỗi là nhìn thấy một khoảng lùi kỳ lạ nơi các tên bị xáo trộn được trộn lẫn và mọi thứ bị xáo trộn trong đầu bạn. Một lần nữa, việc thực hiện thay thế giải quyết điều này. Bạn sẽ thấy các chức năng được đặt tên rõ ràng trong backtraces. Tuy nhiên, swizzling có thể khó gỡ lỗi vì thật khó để nhớ những gì tác động của swizzling đang có. Tài liệu mã của bạn tốt (ngay cả khi bạn nghĩ bạn là người duy nhất sẽ nhìn thấy nó). Thực hiện theo các thực hành tốt, và bạn sẽ ổn thôi. Nó không khó để gỡ lỗi hơn mã đa luồng.
Phần kết luận
Phương pháp swizzling là an toàn nếu sử dụng đúng cách. Một biện pháp an toàn đơn giản bạn có thể thực hiện là chỉ lướt qua load
. Giống như nhiều thứ trong lập trình, nó có thể nguy hiểm, nhưng hiểu được hậu quả sẽ cho phép bạn sử dụng nó đúng cách.
1 Sử dụng phương pháp làm mờ được xác định ở trên, bạn có thể làm cho mọi thứ trở nên an toàn nếu bạn sử dụng trampolines. Bạn sẽ cần hai tấm bạt lò xo. Khi bắt đầu phương thức, bạn sẽ phải gán con trỏ hàm store
, cho một hàm quay cho đến khi địa chỉ được store
trỏ đến thay đổi. Điều này sẽ tránh mọi điều kiện cuộc đua trong đó phương thức bị xáo trộn được gọi trước khi bạn có thể đặt store
con trỏ hàm. Sau đó, bạn sẽ cần sử dụng một tấm bạt lò xo trong trường hợp việc triển khai chưa được xác định trong lớp và có tra cứu tấm bạt lò xo và gọi phương thức siêu hạng đúng cách. Xác định phương thức để nó tự động tra cứu siêu triển khai sẽ đảm bảo rằng thứ tự các cuộc gọi bị trượt không thành vấn đề.