나는 이것이 정말로 좋은 질문이라고 생각하며, 실제 질문을 다루기보다는 대부분의 답변이 문제를 겪고 단순히 스위 즐링을 사용하지 말라고 말한 것은 부끄러운 일입니다.
방법 지글 지글을 사용하는 것은 부엌에서 날카로운 칼을 사용하는 것과 같습니다. 어떤 사람들은 날카롭게자를 것이라고 생각하기 때문에 날카로운 칼이 무서워하지만, 진실은 날카로운 칼이 더 안전 하다는 것입니다 .
메소드 스위 즐링을 사용하면 더 효율적이고 유지 보수가 용이 한 코드를 작성할 수 있습니다. 또한 악용 될 수 있으며 끔찍한 버그로 이어질 수 있습니다.
배경
모든 디자인 패턴과 마찬가지로 패턴의 결과를 완전히 알고 있으면 패턴 사용 여부에 대한 정보에 근거한 결정을 내릴 수 있습니다. 싱글 톤은 논란의 여지가있는 좋은 예이며, 정당한 이유 때문에 제대로 구현하기가 어렵습니다. 많은 사람들이 여전히 싱글 톤을 사용하기로 선택합니다. 스위 즐링에 대해서도 마찬가지입니다. 선과 악을 완전히 이해하면 자신의 의견을 제시해야합니다.
토론
다음은 메소드 스위 즐링의 함정 중 일부입니다.
- 방법 스위 즐링은 원자가 아니다
- 소유하지 않은 코드의 동작 변경
- 가능한 이름 충돌
- 회전하면 메소드의 인수가 변경됩니다.
- 지글 지글 순서가 중요합니다
- 이해하기 어려움 (재귀 적으로 보입니다)
- 디버깅하기 어려움
이 요점은 모두 유효하며,이를 해결하기 위해 방법 스위 즐링에 대한 이해와 결과를 달성하는 데 사용 된 방법론을 모두 개선 할 수 있습니다. 한 번에 하나씩 가져 가겠습니다.
방법 스위 즐링은 원자가 아니다
나는 동시에 사용하는 것이 안전한 메소드 스위 즐링의 구현을 아직 보지 못했다 1 . 메소드 스위 즐링을 사용하려는 경우의 95 %에서는 실제로 문제가되지 않습니다. 일반적으로 메소드의 구현을 대체하기를 원하며 해당 구현이 프로그램의 전체 수명 동안 사용되기를 원합니다. 이것은에서 당신의 방법 스위 즐링을해야한다는 것을 의미합니다 +(void)load
. load
클래스 메소드는 응용 프로그램의 시작 부분에 순차적으로 실행된다. 여기서 스위 즐링을 수행하면 동시성에 문제가 없습니다. +(void)initialize
그러나 swizzle에서 스위 즐링을 수행하면 스위 즐링 구현에서 경쟁 조건이 발생하고 런타임이 이상한 상태가 될 수 있습니다.
소유하지 않은 코드의 동작 변경
이것은 스위 즐링의 문제이지만, 요점입니다. 목표는 해당 코드를 변경할 수있는 것입니다. 사람들이 이것을 큰 문제로 지적하는 이유는 변경 NSButton
하려는 인스턴스 하나만 변경하는 것이 아니라 NSButton
애플리케이션의 모든 인스턴스 를 변경하기 때문입니다 . 이러한 이유로, 당신이 쓸어 넘길 때 조심해야하지만 완전히 피할 필요는 없습니다.
이런 식으로 생각하십시오 ... 클래스의 메소드를 재정의하고 수퍼 클래스 메소드를 호출하지 않으면 문제가 발생할 수 있습니다. 대부분의 경우 수퍼 클래스는 해당 메소드가 호출 될 것으로 예상합니다 (달리 문서화되어 있지 않는 한). 이 같은 생각을 스위 즐링에 적용하면 대부분의 문제를 다룰 수 있습니다. 항상 원래 구현을 호출하십시오. 그렇지 않은 경우 안전을 위해 너무 많이 변경되었을 수 있습니다.
가능한 이름 충돌
이름 충돌은 Cocoa 전체에서 문제입니다. 우리는 종종 클래스 이름과 메소드 이름을 범주에 접두어로 붙입니다. 불행히도, 이름 충돌은 우리 언어의 전염병입니다. 그러나 스위 즐링의 경우에는 반드시 그럴 필요는 없습니다. 메소드 스위 즐링에 대해 생각하는 방식을 약간 변경하면됩니다. 대부분의 지글 지글은 다음과 같이 수행됩니다.
@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:
다른 곳에서 정의 되면 어떻게됩니까? 이 문제는 지글 지글 만의 문제는 아니지만 어쨌든 해결할 수 있습니다. 이 해결 방법에는 다른 함정을 해결하는 추가 이점이 있습니다. 우리가 대신하는 일은 다음과 같습니다.
@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와 약간 비슷하지만 이름 충돌을 피합니다. 원칙적으로 표준 스위 즐링과 똑같은 작업을 수행합니다. 이것은 한동안 정의 된대로 스위 즐링을 사용하는 사람들에게는 약간의 변화 일지 모르지만 결국에는 더 낫다고 생각합니다. 스위 즐링 방법은 다음과 같이 정의됩니다.
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
메소드의 이름을 바꾸어 스위블하면 메소드의 인수가 변경됩니다.
이것은 내 마음에 큰 것입니다. 이것이 표준 방법 스위 즐링을 수행하지 않아야하는 이유입니다. 원래 메소드의 구현으로 전달 된 인수를 변경하고 있습니다. 이것이 일어나는 곳입니다.
[self my_setFrame:frame];
이 라인의 기능은 다음과 같습니다.
objc_msgSend(self, @selector(my_setFrame:), frame);
런타임을 사용하여의 구현을 찾습니다 my_setFrame:
. 구현이 발견되면 제공된 것과 동일한 인수로 구현을 호출합니다. 찾은 구현은의 원래 구현 setFrame:
이므로 계속해서 호출하지만 _cmd
인수가 그렇지 setFrame:
않아야합니다. 지금 my_setFrame:
입니다. 원래 구현은 결코 예상하지 못한 인수로 호출됩니다. 이건 좋지 않아
간단한 해결책이 있습니다. 위에서 정의한 대체 스위 즐링 기법을 사용하십시오. 논쟁은 변하지 않을 것입니다!
지글 지글 순서가 중요합니다
방법이 뒤섞인 순서가 중요합니다. 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
되나요? 대부분의 스위 즐링은 setFrame:
모든 뷰 의 구현을 대체하지 않도록 보장 하므로 인스턴스 메소드를 가져옵니다 . 이에 기존의 구현을 사용합니다 재 - 정의 setFrame:
에서 NSButton
구현을 교환하는 것은 모든 뷰에 영향을주지 않도록 클래스입니다. 기존 구현은에 정의 된 구현입니다 NSView
. 스위 즐링을 할 때도 동일한 일이 발생합니다 NSControl
(다시 NSView
구현 사용).
setFrame:
버튼 을 호출하면 swizzled 메소드를 호출 한 다음 setFrame:
원래에 정의 된 메소드로 바로 이동합니다 NSView
. NSControl
및 NSView
스위 즐링 구현은 호출되지 않습니다.
그러나 주문이 다음과 같은 경우 :
[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
뷰 스위 즐링이 먼저 발생하기 때문에 컨트롤 스위 즐링이 올바른 방법 을 끌어 올릴 수 있습니다 . 마찬가지로 컨트롤 스위 즐링이 버튼 스위 즐링 이전에 있었기 때문에 버튼은 컨트롤의 스위블 된 구현을 끌어 올립니다setFrame:
. 이것은 약간 혼란 스럽지만 올바른 순서입니다. 이 순서를 어떻게 보장 할 수 있습니까?
다시 말하지만, load
사물을 쓸어 넘기십시오. 강타를 load
하고로드중인 클래스 만 변경하면 안전합니다. 이 load
메소드는 서브 클래스 전에 수퍼 클래스로드 메소드가 호출되도록합니다. 우리는 정확한 순서를 얻을 것입니다!
이해하기 어려움 (재귀 적으로 보입니다)
전통적으로 정의 된 swizzled 방법을 살펴보면 실제로 무슨 일이 일어나고 있는지 말하기가 어렵다고 생각합니다. 그러나 우리가 위즐 링을 한 다른 방법을 살펴보면 이해하기 쉽습니다. 이것은 이미 해결되었습니다!
디버깅하기 어려움
디버깅 중 혼란 중 하나는 뒤섞인 이름이 섞여서 머리가 뒤죽박죽이되는 이상한 역 추적을 보는 것입니다. 다시, 대체 구현이이를 해결합니다. 역 추적에 명확하게 명명 된 함수가 표시됩니다. 그러나 스위 즐링의 영향을 기억하기 어렵 기 때문에 스위 즐링을 디버깅하기가 어려울 수 있습니다. 코드를 잘 볼 수있는 사람이라고 생각하더라도 코드를 잘 문서화하십시오. 모범 사례를 따르면 괜찮을 것입니다. 멀티 스레드 코드보다 디버깅하기가 어렵지 않습니다.
결론
올바르게 사용하면 방법 스위 즐이 안전합니다. 당신이 취할 수있는 간단한 안전 조치는 안으로 만 흔들리는 것입니다 load
. 프로그래밍의 많은 것들과 마찬가지로 위험 할 수 있지만 결과를 이해하면 올바르게 사용할 수 있습니다.
1 위에서 정의한 스위 즐링 방법을 사용하면 트램폴린을 사용하는 경우 스레드를 안전하게 만들 수 있습니다. 두 개의 트램폴린이 필요합니다. 메소드 시작시, store
주소 store
가 변경 될 때까지 회전하는 함수에 함수 포인터를 지정해야합니다 . 이것은 store
함수 포인터 를 설정하기 전에 swizzled 메소드가 호출 된 경쟁 조건을 피합니다 . 그런 다음 구현이 클래스에 아직 정의되어 있지 않은 경우 트램펄린을 사용해야하고 트램펄린을 조회하고 수퍼 클래스 메소드를 올바르게 호출해야합니다. 메소드를 정의하여 수퍼 구현을 동적으로 조회하면 스위블 링 호출 순서가 중요하지 않습니다.