Objective-C에서 메소드 스위블의 위험은 무엇입니까?


235

사람들이 방법 스위 즐링이 위험한 습관이라고 말한 것을 들었습니다. 스위 즐링이라는 이름조차도 약간의 속임수임을 암시합니다.

메소드 스위블 은 선택기 A를 호출하면 실제로 구현 B를 호출하도록 맵핑을 수정합니다.이를 사용하는 한 가지 방법은 닫힌 소스 클래스의 동작을 확장하는 것입니다.

우리는 스위 즐링 사용 여부를 결정하는 사람이 자신이하려는 일에 가치가 있는지에 대한 정보에 근거한 결정을 내릴 수 있도록 위험을 공식화 할 수 있습니까?

예 :

  • 이름 지정 충돌 : 나중에 클래스가 추가 한 메소드 이름을 포함하도록 기능을 확장하면 큰 문제가 발생합니다. 현명하게 스위블 된 방법의 이름을 지정하여 위험을 줄입니다.

읽은 코드에서 기대하는 다른 것을 얻는 것은 이미 좋지 않습니다
Martin Babacaev

이것은 파이썬 데코레이터가 매우 깨끗하게하는 일을 부정한 방법으로 들립니다
Claudiu

답변:


439

나는 이것이 정말로 좋은 질문이라고 생각하며, 실제 질문을 다루기보다는 대부분의 답변이 문제를 겪고 단순히 스위 즐링을 사용하지 말라고 말한 것은 부끄러운 일입니다.

방법 지글 지글을 사용하는 것은 부엌에서 날카로운 칼을 사용하는 것과 같습니다. 어떤 사람들은 날카롭게자를 것이라고 생각하기 때문에 날카로운 칼이 무서워하지만, 진실은 날카로운 칼이 더 안전 하다는 것입니다 .

메소드 스위 즐링을 사용하면 더 효율적이고 유지 보수가 용이 한 코드를 작성할 수 있습니다. 또한 악용 될 수 있으며 끔찍한 버그로 이어질 수 있습니다.

배경

모든 디자인 패턴과 마찬가지로 패턴의 결과를 완전히 알고 있으면 패턴 사용 여부에 대한 정보에 근거한 결정을 내릴 수 있습니다. 싱글 톤은 논란의 여지가있는 좋은 예이며, 정당한 이유 때문에 제대로 구현하기가 어렵습니다. 많은 사람들이 여전히 싱글 톤을 사용하기로 선택합니다. 스위 즐링에 대해서도 마찬가지입니다. 선과 악을 완전히 이해하면 자신의 의견을 제시해야합니다.

토론

다음은 메소드 스위 즐링의 함정 중 일부입니다.

  • 방법 스위 즐링은 원자가 아니다
  • 소유하지 않은 코드의 동작 변경
  • 가능한 이름 충돌
  • 회전하면 메소드의 인수가 변경됩니다.
  • 지글 지글 순서가 중요합니다
  • 이해하기 어려움 (재귀 적으로 보입니다)
  • 디버깅하기 어려움

이 요점은 모두 유효하며,이를 해결하기 위해 방법 스위 즐링에 대한 이해와 결과를 달성하는 데 사용 된 방법론을 모두 개선 할 수 있습니다. 한 번에 하나씩 가져 가겠습니다.

방법 스위 즐링은 원자가 아니다

나는 동시에 사용하는 것이 안전한 메소드 스위 즐링의 구현을 아직 보지 못했다 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. NSControlNSView스위 즐링 구현은 호출되지 않습니다.

그러나 주문이 다음과 같은 경우 :

[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 메소드가 호출 된 경쟁 조건을 피합니다 . 그런 다음 구현이 클래스에 아직 정의되어 있지 않은 경우 트램펄린을 사용해야하고 트램펄린을 조회하고 수퍼 클래스 메소드를 올바르게 호출해야합니다. 메소드를 정의하여 수퍼 구현을 동적으로 조회하면 스위블 링 호출 순서가 중요하지 않습니다.


24
놀랍도록 유익하고 기술적으로 확실한 답변. 토론은 정말 흥미 롭습니다. 시간을내어 작성해 주셔서 감사합니다.
Ricardo Sanchez-Saez 2016 년

앱 스토어에서 경험에 대한 지글 지글을 받아 들일 수 있는지 지적 해 주시겠습니까? stackoverflow.com/questions/8834294 로 안내합니다 .
Dickey Singh

3
스위블 링은 App Store에서 사용할 수 있습니다. 많은 앱과 프레임 워크가 포함되어 있습니다. 위의 모든 내용은 여전히 ​​유지되며 개인 메소드를 스위 즐 할 수 없습니다 (실제로 기술적으로 할 수는 있지만 거부 할 위험이 있으며 다른 위험이 있습니다).
wbyoung

놀라운 답변입니다. 그러나 왜 + swizzle : with : store :에서 직접 대신 class_swizzleMethodAndStore ()에서 스위 즐링을 만드는가? 이 추가 기능이 필요한 이유는 무엇입니까?
Frizlab

2
@ 프리즈 랩 좋은 질문! 정말 스타일 일뿐입니다. Objective-C 런타임 API (C)에서 직접 작동하는 많은 코드를 작성하는 경우 일관성을 위해 C 스타일을 호출 할 수 있습니다. 내가 이것 이외에도 생각할 수있는 유일한 이유는 순수한 C로 무언가를 쓴다면 더 호출 가능하기 때문입니다. 그러나 Objective-C 방법으로 모든 것을 할 수있는 이유는 없습니다.
wbyoung

12

먼저 스위 즐링 방법으로 의미하는 바를 정확하게 정의하겠습니다.

  • 원래 메소드 (A)로 전송 된 모든 호출을 새 메소드 (B)로 다시 라우팅합니다.
  • 우리는 방법 B를 소유합니다
  • 우리는 방법 A를 소유하지 않습니다
  • 메소드 B는 일부 작업을 수행 한 후 메소드 A를 호출합니다.

메소드 스위 즐링이 이것보다 더 일반적이지만 이것이 내가 관심있는 경우입니다.

위험 :

  • 원래 수업의 변화 . 우리는 우리가 스위 즐링하는 수업을 소유하지 않습니다. 수업이 바뀌면 지글 지글 작업이 중단 될 수 있습니다.

  • 유지하기 어렵다 . swizzled 방법을 작성하고 유지해야 할뿐만 아니라 스위 즐을 수행하는 코드를 작성하고 유지해야합니다

  • 디버깅하기 어렵다 . 스위 즐의 흐름을 따라 가기가 어렵습니다. 일부 사람들은 스위 즐이 수행되었다는 사실조차 깨닫지 못할 수도 있습니다. swizzle에서 버그가 발생했을 경우 (아마도 원래 클래스의 변경으로 인해) 해결하기가 어렵습니다.

요약하자면, 스위 즐링을 최소한으로 유지하고 원래 수업의 변화가 어떻게 스위 즐에 영향을 줄 수 있는지 고려해야합니다. 또한 현재하고있는 일을 명확하게 설명하고 문서화해야합니다 (또는 완전히 피하십시오).


@everyone 나는 당신의 답변을 멋진 형식으로 결합했습니다. 자유롭게 편집하거나 추가하십시오. 입력 해 주셔서 감사합니다.
Robert

나는 당신의 심리학의 팬입니다. "훌륭한 스위 즐링 능력으로 엄청난 스위 즐링 책임이 따른다."
Jacksonkr

7

정말 위험한 스위 즐링 자체는 아닙니다. 문제는 당신이 말했듯이 프레임 워크 클래스의 동작을 수정하는 데 종종 사용된다는 것입니다. 개인 클래스가 "위험한"작동 방식에 대해 알고 있다고 가정합니다. 오늘 수정 사항이 적용 되더라도 Apple이 나중에 수업을 변경하여 수정 사항이 중단 될 가능성은 항상 있습니다. 또한 많은 다른 앱이 그렇게한다면 Apple이 기존 소프트웨어를 많이 손상시키지 않으면 서 프레임 워크를 변경하기가 훨씬 더 어려워집니다.


그런 개발자들은 파종 한 것을 거두어야한다고 말하고 있습니다. 그들은 코드를 특정 구현에 밀접하게 결합했습니다. 따라서 코드를 제대로 결합하지 않겠다는 명백한 결정이 아니라면 코드가 깨지지 않아야하는 미묘한 방식으로 구현이 변경되는 것이 잘못입니다.
Arafangion

그러나 다음 버전의 iOS에서는 매우 인기있는 앱이 작동하지 않는다고 말합니다. 개발자의 잘못이라고 말하기는 쉽지만 더 잘 알고 있어야하지만 Apple은 사용자의 눈에 좋지 않게 보입니다. 코드를 다소 혼란스럽게 만들 수 있다는 점을 제외하고는 자신의 메소드 또는 클래스를 스위 즐링하는 데 아무런 해가 없습니다. 다른 사람이 제어하는 ​​코드를 쓸어 넘기면 조금 더 위험 해지기 시작합니다. 스위 즐링이 가장 유혹적 인 상황입니다.
Caleb

애플은 siwzzling을 통해 수정할 수있는 기본 클래스를 제공하는 유일한 업체는 아닙니다. 모든 사람을 포함하도록 귀하의 답변을 편집해야합니다.
AR

@AR 어쩌면 질문은 iOS와 iPhone으로 태그되어 있으므로 해당 맥락에서 고려해야합니다. iOS 앱이 iOS에서 제공하는 것 이외의 라이브러리 및 프레임 워크를 정적으로 연결해야한다는 점을 감안할 때 Apple은 실제로 앱 개발자의 참여없이 프레임 워크 클래스의 구현을 변경할 수있는 유일한 엔티티입니다. 그러나 나는 비슷한 문제가 다른 플랫폼에 영향을 미친다는 점에 동의하며, 조언은 스위 즐링을 넘어서 일반화 될 수 있다고 말합니다. 일반적으로 조언은 다음과 같습니다 . 다른 사람이 유지 관리하는 구성 요소를 수정하지 마십시오.
Caleb

방법 스위 즐링은 그렇게 위험 할 수 있습니다. 그러나 프레임 워크가 나오면 이전 프레임 워크를 완전히 생략하지는 않습니다. 여전히 앱이 기대하는 기본 프레임 워크를 업데이트해야합니다. 이 업데이트로 인해 앱이 손상 될 수 있습니다. iOS가 업데이트 될 때 큰 문제는 아닐 것입니다. 내 사용 패턴은 기본 reuseIdentifier를 무시하는 것이 었습니다. _reuseIdentifier 변수에 액세스 할 수 없었기 때문에 그것이 제공되지 않는 한 변수를 바꾸고 싶지 않았기 때문에 유일한 해결책은 모든 것을 서브 클래스 화하고 재사용 식별자를 제공하는 것이지, nil이면 swizzle 및 override였습니다. 구현 유형이 핵심입니다 ...
Lazy Coder

5

신중하고 현명하게 사용하면 우아한 코드로 이어질 수 있지만 일반적으로 혼란스러운 코드로 이어집니다.

특정 디자인 작업에 매우 우아한 기회를 제공한다는 사실을 알지 못하면 금지되어야하지만 상황에 잘 적용되는 이유와 대안이 상황에 맞게 우아하게 작동하지 않는 이유를 분명히 알아야합니다. .

예를 들어, 방법 스위 즐링의 좋은 적용 중 하나는 스위 즐링 (Swizzling)인데, 이는 ObjC가 Key Value Observing을 구현하는 방법입니다.

나쁜 예는 클래스를 확장하는 수단으로 메소드 스위 즐링에 의존하여 매우 높은 커플 링을 초래할 수 있습니다.


1
메소드 스위 즐링 의 맥락에서 KVO를 언급하면 ​​-1입니다 . isa swizzling은 다른 것입니다.
Nikolai Ruhe

@NikolaiRuhe : 깨달을 수 있도록 참조를 제공하십시오.
Arafangion

다음 은 KVO구현 세부 사항에 대한 참조 입니다 . 방법 스위 즐링 은 CocoaDev에 설명되어 있습니다.
Nikolai Ruhe

5

이 기술을 사용했지만 다음 사항을 지적하고 싶습니다.

  • 문서화되지 않았지만 원하는 부작용을 일으킬 수 있기 때문에 코드를 난독 처리합니다. 코드를 읽을 때 코드 기반을 검색하여 코드가 흔들 렸는지 확인하지 않는 한 필요한 부작용 동작을 알지 못할 수 있습니다. 코드가 부작용 swizzled behavior에 의존하는 모든 장소를 항상 문서화하는 것이 항상 가능하지는 않기 때문에이 문제를 완화시키는 방법을 잘 모르겠습니다.
  • 다른 곳에서 사용하려는 스위블 된 동작에 의존하는 코드 세그먼트를 찾는 사람은 스위블 된 방법을 찾아서 복사하지 않고 코드를 잘라내어 다른 코드베이스에 붙여 넣을 수 없기 때문에 코드의 재사용 성을 떨어 뜨릴 수 있습니다.

4

가장 큰 위험은 우연히 많은 원치 않는 부작용을 일으키는 것입니다. 이러한 부작용은 스스로를 '버그'로 표시하여 솔루션을 찾기위한 잘못된 경로로 이어질 수 있습니다. 내 경험상 위험은 읽을 수없고 혼란스럽고 실망스러운 코드입니다. 누군가가 C ++에서 함수 포인터를 과도하게 사용하는 경우와 비슷합니다.


문제는 디버깅하기 어렵다는 것입니다. 이 문제는 검토자가 코드가 흔들리고 디버깅 할 때 잘못된 위치를 찾고 있음을 인식하지 못하기 때문에 발생합니다.
Robert

3
그렇습니다. 또한, 남용되면 끝없는 체인이나 거미줄에 빠질 수 있습니다. 미래에 어느 시점에서 그 중 하나만 바꾸면 어떻게 될지 상상해보십시오. 나는 찻잔을 쌓거나 '코드 젠가'를 연주하는 경험을 좋아한다
AR

4

이상한 코드로 끝날 수 있습니다.

- (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 마술과 관련된 실제 프로덕션 코드에서.


3

단위 테스트에서 메소드 스위 즐링이 매우 유용 할 수 있습니다.

모의 객체를 작성하고 실제 객체 대신 해당 모의 객체를 사용할 수 있습니다. 코드를 깨끗하게 유지하고 단위 테스트에 예측 가능한 동작이 있습니다. CLLocationManager를 사용하는 일부 코드를 테스트하려고한다고 가정하겠습니다. 단위 테스트는 startUpdatingLocation을 스 와이프하여 미리 지정된 위치 세트를 대리인에게 제공하고 코드를 변경할 필요가 없습니다.


isa-swizzling이 더 나은 선택입니다.
vikingosegundo
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.