2015년 1월 12일 월요일

[Objective-C] performSelector에서 메모리 릭(leak) 경고가 뜬다?

오랫만에 Objective-C 전용 글. 구시대(?) 방식으로 performSelector를 쓰는 경우 요즘은 아래와 같은 식의 빌드 경고가 발생 할 수 있다.

PerformSelector may cause a leak because its selector is unknown

일단 이런 경고가 발생하는 예제를 보자. 아래 예제는 UIButton의 터치 이벤트를 받기 위해 코드로 종종 사용하는 addTarget:action:forControlEvent: 메소드의 모양을 흉내내고 있다고 생각하자.
// Definitions (.h) --------------

@interface SomeClass : NSObject

@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL action;

- (void)setTarget:(id)target action:(SEL)action;
- (void)triggerAction;

@end

// Implementations (.m) --------------

@implementation SomeClass

- (void)setTarget:(id)target action:(SEL)action {
    self.target = target;
    self.action = action;
}

- (void)triggerAction {
    [self.target performSelector:self.action withObject:self];
}

@end
여기서 아래에 있는 triggerAction 이라는 메소드 내부에서 performSelector를 호출한다. 이 코드를 빌드해 보면 앞서 이야기 한 경고가 발생한다.


이 경고 내용은 '셀렉터를 알 수가 없기 때문에 릭이 생길 수 있다' 는 것이다. 정확한 문제는 잘 모르겠지만 상상을 해 보자. ARC 에서는 타입을 명확히 알아야 리테인 여부를 컴파일러가 파악 할 수 있는데 그것이 불가능 하다보니 쓸 데 없는 리테인이 발생하는 것이 아닐까 생각된다. 이런 현상이 발생되면 상황에 따라 릴리즈 되지 못 하는 경우도 있을 것이다. 물론 상상일 뿐이다. ;-)

경고이기 때문에 빌드나 실행 자체에는 문제가 없을 것이다. 하지만 개인적으로 경고가 나는 상황은 올바르지 않다고 보고, 이런 경고를 무시하다가 원인 불명의 버그가 생길 수도 있기에 해결하는 것을 추천한다.

여러 가지 해결 방법을 살펴보자.

methodForSelector의 활용

기존의 코드를 아래 처럼 methodForSelector를 이용해 함수 처럼 재가공 하는 방법이 있다.
- (void)triggerAction {
    IMP imp = [self.target methodForSelector:self.action];
    void (*func)(id, SEL, id) = (void *)imp;
    func(self.target, self.action, self);
}
이렇게 하면 명시적으로 셀렉터의 모양을 결정 해 줄 수 있기 때문에 가장 좋은 방법이라고 생각된다.

여기서 애매한 것은 2, 3번째 줄의 모양이다.
void (*func)(id, SEL, id) = (void *)imp;
위 'func' 는 함수 포인터의 이름이다. 이 이름은 마음껏 지어도 된다. 그리고 그 뒤로 이어지는 3개의 파라미터 타입 선언 중 첫 두 개는 필수적으로 있어야 하고 마지막 id 타입이 기존 performSelector에서 withObject로 넘어가던 그 오브젝트타입이라는 의미이다.
func(self.target, self.action, self);
이 라인은 앞서 만들어 둔 함수포인터를 호출하는 구문이다. 정의와 일치하게 셀렉터가 존재하는 오브젝트, 셀렉터, 그리고 마지막에 넘겨줄 오브젝트를 명시하는 식으로 호출한다. 첫 두 개의 파라미터 선언이 필수인 이유는 셀렉터가 존재하는 오브젝트와 셀렉터 자체를 넘겨주지 않으면 호출이 되지 않기 때문이다.

참고로 이렇게 3 줄로 만든 코드를 아래처럼 한 줄로 만들어 버릴 수도 있다.
((void (*)(id, SEL, id))[self.target methodForSelector:self.action])(self.target, self.action, self);
한 줄로 쓰면 복잡하고 라인이 길어지기 때문에 가독성이 떨어진다. 앞서 본 3줄로 풀어 쓰는게 낫다는 느낌이다. -_-;

NSInvocation의 활용

NSInvocation을 이용해 셀렉터를 호출하는 방법도 있다.
- (void)triggerAction {
    NSInvocation *inv = [NSInvocation invocationWithMethodSignature:[self.target methodSignatureForSelector:self.action]];
    [inv setTarget:self.target];
    [inv setSelector:self.action];
    id sender = self;
    [inv setArgument:&sender atIndex:2];
    [inv invoke];
}
뭔가 좀 복잡해지는 기분이지만 어쨌든 동작은 잘 된다.

경고를 아예 무시해 버리기

좀 찝찝하지만 경고를 안뜨게 만들어 버리는 방법도 있다.
- (void)triggerAction {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    [self.target performSelector:self.action withObject:self];
#pragma clang diagnostic pop
}
이 방법은 그다지 추천하고 싶지는 않다. 정말 릭이 발생 할 지도 모르니까.

이 외에 objc_msgSend를 이용하는 식도 있겠지만 최신 Xcode에서는 왜인지 함수 정의(Definition)가 없다는 오류가 발생해서 테스트를 못 해 봐서 생략한다.

댓글 없음 :