본문 바로가기
프로그래밍 놀이터/iOS

[Effective Objective-C] #52 NSTimer 가 타깃을 리테인한다는 사실을 기억하라

by 돼지왕 왕돼지 2017. 10. 20.
반응형

 [Effective Objective-C] #52 NSTimer 가 타깃을 리테인한다는 사실을 기억하라


출처 : Effective Objective-C

absolute date, Category, interval, invalidate, invalidated, NSTimer, nstimer block, nstimer block retain cycle, nstimer retain, nstimer retain cycle, run loop, scheduledTimerWithTimeInterval, selector, Target, weak, [Effective Objective-C] #52 NSTimer 가 타깃을 리테인한다는 사실을 기억하라, __weak, 리테인 순환, 반복, 반복 주기, 블록, 스케줄, 스케줄링, 실행 루프, 싱글턴 객체, 이벤트 발생, 절대 시간, 타이머, 타이머 반복, 폴링


-

NSTimer 클래스는 절대 날짜(absolute date)와 시간 또는 주어진 시간 뒤에 실행되도록 스케줄될 수 있다.

타이머는 또한 반복할 수 있다.

그렇기 때문에 얼마나 자주 발생시켜야 할지 정의하는 반복 주기(interval)이 있다.

예를 들어 리소스를 5초마다 폴링하기 위해 타이머를 사용할 수 있다.



-

타이머는 실행 루프(run loop)와 연관되어 있다.

실행 루프는 타이머가 이벤트를 발생시켜야 할 때를 다룬다.

타이머를 생성할 때 현재 실행 루프에 스케줄 된 상태로 생성할 수 있다.

또는 타이머를 생성하여 직접 실행 루프에 스케줄시킬 수도 있다.

어떤 방법으로 타이머를 실행하든 타이머는 실행 루프에 스케줄 되었을 때만 이벤트를 발생시킨다.



-

+(NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds target:(id)taget selector:(SEL)selector userInfo:(id)userInfo repeats:(BOOL)repeats


이 메서드를 이용해 일정 시간 뒤에 이벤트를 발생시키는 타이머를 생성할 수 있다.

선택적으로 타이머를 수동으로 멈출 때까지 반복적으로 이벤트를 발생시키게 할 수도 있다.

target 과 selector 는 타이머가 이벤트를 발생시켰을 때 호출될 목표 객체와 호출될 선택자를 말한다.

타이머는 타깃을 리테인한다.

그리고 타이머가 종료되면(invalidated) 타깃을 릴리스할 것이다.

타이머는 invalidate 가 호출되거나 이벤트가 발생되면 종료된다.

타이머가 반복하도록 설정했으면 여러분이 원할 때 종료시킬 수 있다.



-

타이머가 타깃을 리테인하기 때문에 반복하는 타이머가 앱에서 문제를 일으킬 수도 있다.

이는 반복하는 타이머에서 리테인 순환 문제가 생길 수 있음을 의미한다.


이 문제를 해결할 수 있는 유일한 방법은 블록을 사용하는 것이다.

@interface NSTimer (EOCBlocksSupport)


+(NSTimer*)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)())block repeats:(BOOL)repeats;


@end


@implementation NSTimer (EOCBlocksSupport)


+(NSTimer*)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)())block repeats:(BOOL)repeats{

     return [self scheduledTimerWithTimeInterval:interval target:self selector:@selector(eoc_blockInvoke:) userInfo:[block copy] repeats:repeats];

}


+(void)eoc_blockInvoke:(NSTimer*)timer{

     void (^block)() = timer.userInfo;

     if ( block ){

          block();

     }

}


@end



이렇게 하는 이유는 리테인 순환 문제를 짧고 간단하게 해결할 수 있기 때문이다.

타이머가 이벤트를 발생시킬 때 실행될 블록은 타이머의 userInfo 파라미터에 설정한다.

이 파라미터는 타이머가 실행되는 동안 리테인되는 불투명한 값이다.

가져간 블록의 복사본은 힙 블록에 반드시 두어야 한다.

그렇게 하지 않으면 나중에 블록을 실행해야 할 때 블록이 사용할 수 없는 상태가 될 수 있다.

타이머의 타깃은 이제 NSTimer 클래스 싱글턴 객체다.

그렇기 때문에 타이머에 의해 리테인돼도 문제가 되지 않는다.

리테인 순환이 있긴 하지만 클래스 객체는 절대 할당 해제하지 않기 때문에 문제가 되지 않는다.


이 방법 자체가 문제를 해결하지는 않지만 문제를 해결할 수 있는 수단을 제공한다.

리테인 순환을 weak 참조를 사용해 깰 수 있다.

-(void)startPolling {

     __weak EOCClass *weakSelf = self;

     _pollTimer = [NSTimer eoc_scheduledTimerWithTimeInterval:5.0 block:^{

          EOCClass *strongSelf = weakSelf;

          [stringSelf p_doPoll];

     } repeats:YES];

}


이 코드는 블록이 일반적인 self 변수가 아닌 약한(weak) self 변수를 잡도록 하는 유용한 패턴을 사용한다.

이렇게 하면 self 는 리테인되지 않지만 블록이 실행되면 즉시 strong 참조가 생성된다.

이는 블록이 살아 있는 동안은 인스턴스가 살아 있는 것을 보장한다.



-

위의 패턴을 사용했으면 EOCClass 의 인스턴스가 가진 마지막 외부 참조가 릴리스될 경우 인스턴스는 할당 해제될 것이다.

할당 해제되는 동안에 타이머가 종료되어 타이머가 더는 실행되지 않는 것을 보장한다.

weak 참조를 사용했기 대문에 훨씬 안전한 사용이 보장된다.




기억할 점


-

NSTimer 객체는 타이머의 이벤트가 발생되거나 명시적인 invalidate 호출을 통해 타이머가 종료되기 전까지 자신의 타깃을 리테인한다.



-

반복하는 타이머를 사용하면서 타이머의 타깃이 타이머를 리테인하면 리테인 순환이 쉽게 발생한다.

이는 객체 그래프의 다른 객체에 의해 직간접적으로 생길 수 있다.



-

NSTimer 가 블록을 사용하도록 확장하면 리테인 순환을 깰 수 있다.

아직 공식적으로 NSTimer 인터페이스에 이 기능이 추가되지 않았기 때문에 현재는 카테고리를 이용해 이 기능을 추가할 수 있다.




반응형

댓글