사람들이 방법 스위 즐링이 위험한 습관이라고 말한 것을 들었습니다. 스위 즐링이라는 이름조차도 약간의 속임수임을 암시합니다.
메소드 스위블 은 선택기 A를 호출하면 실제로 구현 B를 호출하도록 맵핑을 수정합니다.이를 사용하는 한 가지 방법은 닫힌 소스 클래스의 동작을 확장하는 것입니다.
우리는 스위 즐링 사용 여부를 결정하는 사람이 자신이하려는 일에 가치가 있는지에 대한 정보에 근거한 결정을 내릴 수 있도록 위험을 공식화 할 수 있습니까?
예 :
- 이름 지정 충돌 : 나중에 클래스가 추가 한 메소드 이름을 포함하도록 기능을 확장하면 큰 문제가 발생합니다. 현명하게 스위블 된 방법의 이름을 지정하여 위험을 줄입니다.
답변
나는 이것이 정말로 좋은 질문이라고 생각하며, 실제 질문을 다루기보다는 대부분의 답변이 문제를 겪고 단순히 스위 즐링을 사용하지 말라고 말한 것은 부끄러운 일입니다.
방법 지글 지글을 사용하는 것은 부엌에서 날카로운 칼을 사용하는 것과 같습니다. 어떤 사람들은 날카롭게자를 것이라고 생각하기 때문에 날카로운 칼이 무서워하지만, 진실은 날카로운 칼이 더 안전 하다는 것입니다 .
메소드 스위 즐링을 사용하면 더 효율적이고 유지 보수가 용이 한 코드를 작성할 수 있습니다. 또한 악용 될 수 있으며 끔찍한 버그로 이어질 수 있습니다.
배경
모든 디자인 패턴과 마찬가지로 패턴의 결과를 완전히 알고 있으면 패턴 사용 여부에 대한 정보에 근거한 결정을 내릴 수 있습니다. 싱글 톤은 논란의 여지가있는 좋은 예이며, 정당한 이유 때문에 제대로 구현하기가 어렵습니다. 많은 사람들이 여전히 싱글 톤을 사용하기로 선택합니다. 스위 즐링에 대해서도 마찬가지입니다. 선과 악을 완전히 이해하면 자신의 의견을 제시해야합니다.
토론
다음은 메소드 스위 즐링의 함정 중 일부입니다.
- 방법 스위 즐링은 원자가 아니다
- 소유하지 않은 코드의 동작 변경
- 가능한 이름 충돌
- 회전하면 메소드의 인수가 변경됩니다.
- 지글 지글 순서가 중요합니다
- 이해하기 어려움 (재귀 적으로 보입니다)
- 디버깅하기 어려움
이 요점은 모두 유효하며,이를 해결하기 위해 방법 스위 즐링에 대한 이해와 결과를 달성하는 데 사용 된 방법론을 모두 개선 할 수 있습니다. 한 번에 하나씩 가져 가겠습니다.
방법 스위 즐링은 원자가 아니다
나는 동시에 사용하는 것이 안전한 메소드 스위 즐링의 구현을 아직 보지 못했다 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 메소드가 호출 된 경쟁 조건을 피합니다 . 그런 다음 구현이 클래스에 아직 정의되어 있지 않은 경우 트램펄린을 사용해야하고 트램펄린을 조회하고 수퍼 클래스 메소드를 올바르게 호출해야합니다. 메소드를 정의하여 수퍼 구현을 동적으로 조회하면 스위블 링 호출 순서가 중요하지 않습니다.
답변
먼저 스위 즐링 방법으로 의미하는 바를 정확하게 정의하겠습니다.
- 원래 메소드 (A)로 전송 된 모든 호출을 새 메소드 (B)로 다시 라우팅합니다.
- 우리는 방법 B를 소유합니다
- 우리는 방법 A를 소유하지 않습니다
- 메소드 B는 일부 작업을 수행 한 후 메소드 A를 호출합니다.
메소드 스위 즐링이 이것보다 더 일반적이지만 이것이 내가 관심있는 경우입니다.
위험 :
-
원래 수업의 변화 . 우리는 우리가 스위 즐링하는 수업을 소유하지 않습니다. 수업이 바뀌면 지글 지글 작업이 중단 될 수 있습니다.
-
유지하기 어렵다 . swizzled 방법을 작성하고 유지해야 할뿐만 아니라 스위 즐을 수행하는 코드를 작성하고 유지해야합니다
-
디버깅하기 어렵다 . 스위 즐의 흐름을 따라 가기가 어렵습니다. 일부 사람들은 스위 즐이 수행되었다는 사실조차 깨닫지 못할 수도 있습니다. swizzle에서 버그가 발생했을 경우 (아마도 원래 클래스의 변경으로 인해) 해결하기가 어렵습니다.
요약하자면, 스위 즐링을 최소한으로 유지하고 원래 수업의 변화가 어떻게 스위 즐에 영향을 줄 수 있는지 고려해야합니다. 또한 현재하고있는 일을 명확하게 설명하고 문서화해야합니다 (또는 완전히 피하십시오).
답변
정말 위험한 스위 즐링 자체는 아닙니다. 문제는 당신이 말했듯이 프레임 워크 클래스의 동작을 수정하는 데 종종 사용된다는 것입니다. 개인 클래스가 “위험한”작동 방식에 대해 알고 있다고 가정합니다. 오늘 수정 사항이 적용 되더라도 Apple이 나중에 수업을 변경하여 수정 사항이 중단 될 가능성은 항상 있습니다. 또한 많은 다른 앱이 그렇게한다면 Apple이 기존 소프트웨어를 많이 손상시키지 않으면 서 프레임 워크를 변경하기가 훨씬 더 어려워집니다.
답변
신중하고 현명하게 사용하면 우아한 코드로 이어질 수 있지만 일반적으로 혼란스러운 코드로 이어집니다.
특정 디자인 작업에 매우 우아한 기회를 제공한다는 사실을 알지 못하면 금지되어야하지만 상황에 잘 적용되는 이유와 대안이 상황에 맞게 우아하게 작동하지 않는 이유를 분명히 알아야합니다. .
예를 들어, 방법 스위 즐링의 좋은 적용 중 하나는 스위 즐링 (Swizzling)인데, 이는 ObjC가 Key Value Observing을 구현하는 방법입니다.
나쁜 예는 클래스를 확장하는 수단으로 메소드 스위 즐링에 의존하여 매우 높은 커플 링을 초래할 수 있습니다.
답변
이 기술을 사용했지만 다음 사항을 지적하고 싶습니다.
- 문서화되지 않았지만 원하는 부작용을 일으킬 수 있기 때문에 코드를 난독 처리합니다. 코드를 읽을 때 코드 기반을 검색하여 코드가 흔들 렸는지 확인하지 않는 한 필요한 부작용 동작을 알지 못할 수 있습니다. 코드가 부작용 swizzled behavior에 의존하는 모든 장소를 항상 문서화하는 것이 항상 가능하지는 않기 때문에이 문제를 완화시키는 방법을 잘 모르겠습니다.
- 다른 곳에서 사용하려는 스위블 된 동작에 의존하는 코드 세그먼트를 찾는 사람은 스위블 된 방법을 찾아서 복사하지 않고 코드를 잘라내어 다른 코드베이스에 붙여 넣을 수 없기 때문에 코드의 재사용 성을 떨어 뜨릴 수 있습니다.
답변
가장 큰 위험은 우연히 많은 원치 않는 부작용을 일으키는 것입니다. 이러한 부작용은 스스로를 ‘버그’로 표시하여 솔루션을 찾기위한 잘못된 경로로 이어질 수 있습니다. 내 경험상 위험은 읽을 수없고 혼란스럽고 실망스러운 코드입니다. 누군가가 C ++에서 함수 포인터를 과도하게 사용하는 경우와 비슷합니다.
답변
이상한 코드로 끝날 수 있습니다.
- (void)addSubview:(UIView *)view atIndex:(NSInteger)index {
//this looks like an infinite loop but we're swizzling so default will get called
[self addSubview:view atIndex:index];
UI 마술과 관련된 실제 프로덕션 코드에서.