2012년 3월 23일 금요일

iOS Memory warning 대처 코딩방법

원제: How to Program for Low Memory Conditions




View controllers 는 가지고 있는 뷰들을 unload할 것이다. 


현재 실행중인 앱에 모달로 떠 있는 뷰 아래에 가려진 하나 이상의 뷰들은 그들의 뷰 컨트롤러에 의해 unload 될 것이다. 다시 unload된 뷰를 가지고 있는 뷰컨트롤러가 활성화 되면, NIB파일을 다시 로드할 것이다.

당신은 뷰 컨트롤러를 도와 view들을 언로드하도록 viewDidUnload 메서드를 구현해야 할 필요가 있다. 뷰 컨트롤러가 들고 있는 것들 중, retain된 UI 요소들, 즉 버튼, 레이블, text view들 등을 릴리즈해줘야 한다. 기본적으로, retain된 IBOutlet 들을 릴리즈 해 줘야 하고, 그들의 포인터를 nil로 설정해 주어야 한다. 아마 IBOutlets들에 대해 프로퍼티를 대부분 사용할테니 그것들을 nil 해주는 방법은 간단하다.
- (void)viewDidUnload
{
    [super viewDidUnload];
    self.myButton = nil;
    self.someLabel = nil;
}
뷰 컨트롤러가  다시 뷰들을 리로드할 때, NIB를 다시 로드하고, 모든 뷰에 대해 새 인스턴스를 만들게 된다. 만약 예전 뷰들이 아직 남아있다면, 그 뷰들은 다시 사용될 수 없다. 그 뷰들의 포인터들은 새 객체 주소로 덮어 씌어지게 될 것이다. 그렇기 때문에 retain된 UI 요소들을 viewDidUnload에서 릴리즈 해 주지 않을 경우, 매 번 뷰들이 리로드 될 때마다 메모리 릭이 발생하게 될 것이다.

viewDidUnload는 뷰 컨트롤러가 dealloc 될 때 불리지 않는 것에 주의하라! viewDidUnload 함수는 오직 메모리 워닝이 발생할 때만 호출된다. 그렇기 때문에 retain된 객체들을 릴리즈 하는 코드를 똑같이 dealloc함수에서도 적용해 주어야 한다. 난 보통 releaseObjects라는 이름의 함수를 만들고, 양쪽에서 모두 호출한다.

- (void)releaseObjects
{
    [myButton release], myButton = nil;
    [someLabel release], someLabel = nil;
}
 
- (void)viewDidUnload
{
    [super viewDidUnload];
    [self releaseObjects];
    // free up anything else here that you can get rid of
}
 
- (void)dealloc
{
    [self releaseObjects];
    // free any other objects that may live beyond the view
    [super dealloc];
}
releaseObject함수 내에서는 프로퍼티들을 사용하지 않는다는 것에 주의하라! 대신에 개별적으로 객체들을 release해주고 포인터에 nil을 지정해 준다. 왜냐하면 이 함수는 dealloc에서도 불릴 것이기 때문이다. 많은 개발자들이 dealloc함수 내에서 accessor 함수들(역자주: 자동으로 만들어진 getter, setter를 말하는 것 같음.) 을 호출하는 것이 좋지 않다고 생각하고 있다. (프로퍼티를 사용한다면 말이다. )


예제

viewDidUnload와 dealloc 함수가 완전히 같지 않다는 것을 깨닫는 것은 중요한 일이다. 그 둘 모두 view관련된 것들을 릴리즈하는 것은 맞으나, dealloc함수는 좀 더 많은 일을 한다. 뷰 컨트롤러의 뷰가 메모리 이유로 언로드될 때 컨트롤러는 남아 있는다. 이러한 이유로 모든 ivars(objects instance variables: ivars)를 viewDidUnload함수내에서 릴리즈하기를 원하지 않을 것이다.

MainViewController가 "favorite"라는 이름의 NSMutableArray를 가지고 있다고 가정해 보자. 이 뷰 컨트롤러는 FavoritesViewController라는 모달 테이블 뷰를 띄운다. 이 FavoritesViewController는 유저로 하여금 리스트에서 favorites를 지울 수 있게 해 준다. 유저가 favorite를 지울 때 FavoritesViewcontroller는 MainViewController에게 delegate 함수를 통해 리스트가 변했음을 알린다.

만약 MainViewController의 뷰가 FavoritesViewController가 보이고 있는 동안 unload된다면, 우리는 favorites 배열이 릴리즈되는 것을 원하지 않을 것이다. 계속 FavoritesViewcontroller와 favorites배열이 상호작용하고 있기를 바랄 것이다. 반면에, MainViewController가 dealloc 될 때는 favorites 배열이 릴리즈되기를 바랄 것이다.
코드로 표현하면 다음과 같을 것이다.
@interface MainWindowController : UIViewController
@property (nonatomic, retain) NSMutableArray* favorites;
@property (nonatomic, retain) UIButton* someButton;
@property (nonatomic, retain) NSDictionary* lookupTable;
@end
 
@implementation MainViewController
 
@synthesize favorites, someButton, lookupTable;
 
- (id)init
{
    if ((self = [super init]))
    {
        self.favorites = [NSMutableArray arrayWithCapacity:10];
    }
    return self;
}
 
- (void)viewDidLoad
{
    self.someButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
 
    // This is some object that we only need while the view is showing.
    self.lookupTable = [NSDictionary dictionaryWithObjectsAndKeys:.....];
}
 
- (void)releaseObjects
{
    // We don't need these objects anymore if we don't have a view.
    [someButton release], someButton = nil;
    [lookupTable release], lookupTable = nil;
}
 
- (void)viewDidUnload
{
    [super viewDidUnload];
    [self releaseObjects];
 
    // Note: we don't want to release favorites here!
}
 
- (void)dealloc
{
    [self releaseObjects];
    [favorites release];
    [super dealloc];
}
 
// This is the delegate method.
// It may be called when our view is not loaded.
- (void)favoritesViewControllerDidDeleteFavorite:(id)favorite
{
    [favorites removeObject:favorite];
    if ([self isViewLoaded])
    {
        // Update the view...
    }
}
 
@end
대신에, viewDidLoad함수내에 favorites 배열을 다음과 같은 방법으로 초기화할 수 있다.

- (void)viewDidLoad
{
    ...
 
    if (self.favorites == nil)
        self.favorites = [NSMutableArray arrayWithCapacity:10];
}

 반드시 이 배열이 존재하고 있는지 체크를 해야한다. 이제 명백하게 밝혀졌듯이 viewDidLoad함수는 한 번 이상 불릴 수 있기 때문이다.

다른 방법으로는 프로퍼티의 getter형태로 늦은 로딩이 되도록 만드는 것이다.
- (NSMutableArray*)favorites
{
    if (favorites == nil)
        self.favorites = [NSMutableArray arrayWithCapacity:10];
    return favorites;
}
이제, 이 배열을 초기화할 필요가 없어졌다! 다만 이 배열을 사용하고자 할 때 self.favorites와 같은 형태로 호출해야 한다. ( favorites 처럼 단독으로 쓰면 안된다. )

경험상, viewDidUnload함수는 데이터를 가능한한 많이 프리시켜야 한다. 단지 뷰가 활성화 될 때 완전한 상태로 돌아올 수 있도록 해 줄  수 있는 최소한의 상태정보만을 유지시킬 필요가 있다. 위의 예에서 lookupTable같은 경우 유지할 필요가 없다. 대신 favorites는 유지시켜야 한다.


좀 더!

당신의 뷰 컨트롤러는 didReceiveMemoryWarning 함수로 낮은 메모리 상태임을 알게 될 것이다. 그 상황에서 필요없는 데이터들을 이 함수를 오버라이딩 하여 그 안에서 릴리즈 시켜줘야 한다.  app delegate 또한 비슷한 함수 - applicationDidReceiveMemoryWarning를 가지고 있다. 다른 클래스 또한 UIApplicationDidReceiveMemoryWarningNotification 알림 메시지를 통해서 그 상황을 알 수 있다.

메모리 워닝을 받을 때 릴리즈가능한 많은 객체들을 가능한한 많이 릴리즈하라! 그러나 항상 릴리즈 프로세스가 동작하기를 바라지는 않을 것이다. 만약 릴리즈가 필요이상으로 많이 불리게 된다면 추가적인 많은 처리가 필요할 테고, 이는 결국 베터리 소모를 더욱 빠르게 하는 결과를 초래할 것이다.

예를 들어 flashcard 앱은 CardView클래스를 사용한다. UIView의 서브클래스이고 두 개의 서브 뷰를 포함한다: 카드의 뒷면과 카드의 앞면. 처음엔 뒷면이 보인다. 유저가 이것을 클릭하면 카드는 animation과 함께 뒤집힌다.

만약 아이폰이 카드 앞면이 보이고 있을 때 메모리 워닝을 받게 된다면, 앱은 뒷면 이미지를 언로드 할 것이다. 그 상황에서 뒷면은 보이지 않으니까 필요하진 않은 상황이다. 다음 카드가 보일 때에만 뒷면은 자동적으로 다시 메모리로 로드되게 될 것이다.

이럴 때 lazy loading이란 기법이 사용될 수 있다. lazy loading은 많은 상황에서 유용하게 쓰일 수 있는 좋은 기법이다. 약간의 추가적인 로직과 계획이 필요할 테지만, 앱이 자발적으로(spontaneously) 죽어버리는 것보다는 나을 것이다.

프로퍼티와 lazy loading은 잘 어울린다. getter 함수를 lazy loading이 되도록 만들면 된다. 이렇게 하면 항상 프로퍼티를 통해서  객체에 접근하게 된다. 이렇게 구현된 상황에서는 만약 객체가 언로드되었을 때, 이 객체는 다시 필요할 때마다(on-the-fly) 재생성될 것이다.

카드뷰클래스에 대한 예제
@interface CardView : UIView
{
    BOOL showingBackView;
}
 
@property (nonatomic, retain) UIImageView* backView;
@end
 
@implementation CardView
 
@synthesize backView;
 
- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
 
    if (!showingBackView)
    {
        [backView removeFromSuperview];
        [backView release], backView = nil;
    }
}
 
- (UIView*)backView
{
    if (backView == nil)
    {
        self.backView = [[[UIImageView alloc] initWithFrame:...] autorelease];
        backView.image = [UIImage imageNamed:@"Some Image"];
        [self.view addSubview:backView];
    }
    return backView;
}
 
- (void)showBack
{
    self.backView.hidden = NO;
}
 
@end
이제, 내가 뒷면view를 필요로할 때 난 "self.backView" 코드를 통해 이것에 접근할 수 있다. 만약, 이 코드가 아직 로드되지 않은 상태라면 바로 생성될 것이다. 그렇기 때문에 우리는 이 객체를 사용하지 않을 때 안전하게 didReceiveMemoryWarning함수내에서 릴리즈 할 수 있고, 우리가 이것을 사용하고자 할 때 다시 안전하게 불러올 수 있다.

UIImage imageNamed


이미지를 로드할 때 아마도 [UIImage imageNamed] 함수를 사용해서 로드할 것이다. 이 함수는 scenes 뒤에 이미지를 캐쉬한다. (It caches the images behind the scenes.) 한 때 imageNamed함수가 메모리 워닝이 발생하였을 때 캐쉬된 이미지들을 제대로 릴리즈하지 못했었다. 그러나, 듣자하니 요새는 꽤 잘 동작한다고 한다. 혹시나 2.x 버전 대를 타겟으로 개발하지 않는 이상 걱정할 필요는 없어보인다. 
그러나, imageNamed 함수의 캐슁 매커니즘을 신뢰할 수 없다면, 또는 좀 더 이것을 컨트롤 하고 싶다면, 당신만의 간단한 이미지 캐쉬를 만들수도 있다. 예를 들자면, 
@implementation MyImageCache
 
+ sharedInstance
{
    static MyImageCache* instance = nil;
    if (instance == nil)
        instance = [[self alloc] init];
    return instance;
}
 
- (NSMutableDictionary*)dictionary
{
    if (dictionary == nil)
        dictionary = [[NSMutableDictionary dictionaryWithCapacity:10] retain];
    return dictionary;
}
 
- (void)dealloc
{
    [dictionary release];
    [super dealloc];
}
 
- (UIImage*)imageNamed:(NSString*)filename
{
    UIImage* image = [self.dictionary objectForKey:filename];
    if (image != nil)
        return image;
 
    image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:filename ofType:nil]];
    if (image == nil)
        return nil;
 
    [self.dictionary setObject:image forKey:filename];
    return image;
}
 
- (void)flushMemory
{
    [dictionary release], dictionary = nil;
}
 
@end

출처:  http://www.hollance.com/2010/12/how-to-program-for-low-memory-conditions/

댓글 없음:

댓글 쓰기