본문 바로가기

안드로이드

메모리 누수 방지

안드로이드 어플리케이션은 (적어도 T-Mobile G1폰에서는) 힙 크기가 16MB 이다. G1폰에서는 많은 메모리이기도 하지만 어떤 개발자에게는  매우 부족한 메모리다. 이 메모리를 모두 사용할 계획이 없더라도 다른 어플리케이션이 죽지 않고 실행되게 하기 위해 최대한 적은 양을 사용해야 한다. 더 많은 안드로이드 어플리케이션이 메모리에 상주할 수록 사용자들은 더 많은 어플리케이션 교체를 수행 할 것이다. 작업도중 안드로이드 어플리케이션의 메모리를 누수를 경험하게 되고 Context 의 참조를 오래동안 유지하는 실수들을 반복하게된다.

안드로이드에서 Context는 많은 목적을 위해서 사용되지만 대부분 리소스를 억세스하고 로딩하기 위해 사용된다. 이것이 많은 위젯의 생성자가 Context를 파라미터로 요구하는 이유다. 보통 안드로이드 어플리케이션에서 개발자는 두가지 종류의 Context (Activity , Application )을 사용한다. 이것은 통상 개발자가 Context를 필요로 하는 클래스와 메서드에 전달하는 첫번째 것이다.

@override
protected void OnCreate(Bundle state) {
super.OnCreate(state);

TextView label = new TextView(this);
label.setText("Leaks are bad");

setContextView(label);
}

이것은 View가 전체 Activity에 대한 참조를 가지고 있다는 것을 의미하므로 개발자가 만든 Activity가 어떤View에 의해 잡혀 있다는 것을 뜻한다.  (역자 주 : 모든 Activity는 Context 클래스를 상속 받으므로 위의 예제코드에서 new TextView(this)와 같이 Activity의 this를 TextView의 생성자에게 전달하는 것이 가능하다) - 전체 View 계층과 연된된 모든 리소스. 따라서 개발자가 Context 누수를 발생 시킬 경우 (누수란 개발자가 이것을 계속 참조함으로서 GC(가베지 콜렉션)가 수행되는 것을 막는을 경우를 말한다) 개발자는 많은 메모리를 잃게 된다. 조심하지 않으면 전체 Activity 누수가 매우 쉽게 발생할수 있다.

스크린 방향을 변경(가로 혹은 세로로 바꾸는 경우) 하는 경우 디폴트로 시스템은 현재의 Activity를 종료하고 그 상태를 유지하는 새로운 Activity를 생성한다. 그렇게 하는 동안 안드로이드는 리소스로부터 어플리케이션의 UI를 다시 로딩할 것이다. 이제 개발자가 리소스로부터 매번 방향이 변경될때마다 로딩하기를 원하지 않는 매우 큰 이미지를 사용하는 어플리케이션을 작성한다고 가정해보자. 매번 방향이 변경될때마다 이미지를 로딩하지 않고 메모리에 유지하는 가장 쉬운 방법은 정적 필드에 이미지를 유지하는 것이다.

private static Drawable sBackground;

@Override
protected void onCreate(Bundle state) {
super.onCreate(state);

TextView label = new TextView(this);
label.setText("Leaks are bad");

if (sBackground == null) {
sBackground = getDrawable(R.drawable.large_bitmap);
}
label.setBackgroundDrawable(sBackground);

setContentView(label);
}

이 코드는 매우 빠르면서 매우 잘못된 코드다. 첫번째 스크린 방향이 변경될 경우 첫번째 Activity를 누수하게되다.Drawable 이 View에 붙게될때 View는 drawable의 callback 에 의해 설정된다. 위 코드에서 이것은 drawable이 TextView를 참조하는 것을 의미하고 TextView는 Activity(Context)를 참조하는 것을 의미하는데 Activity는 코드의 내용에 따라 많은 다른 것들을 역시 참조하고 있을 것이다.

이 예제는 Context 누수의 가장 단순한 예제이며  홈스크린 소스코드 에서 이문제를 어떻해 해결하는 지 볼수 있다. (unbindDrawables() 메소드를 보자) - Activity가 destory될 때 저장된 drawables의 callback을 null로 설정하여 해결하였다. 매우 흥미롭게도 context 누수 체인을 발생시킬수 있는 곳은 매우 많고 누수는 더 빨리 메모리 부족 현상을 야기할 시킬 것이므로 좋지 않다.

Context 관련 메모리 누수를 회피할수 있는 2가지 쉬운 방법이 있는데, 가장 명확한 방법은 자신의 범위(Scope) 바깥쪽으로 context를 벗어나지 않는 것이다. 위 예제는 정적 참조의 경우를 보여주는 것이지만, inner 클래스내에서 참조하는 경우와 inner클래스에서 암시적으로 outer 클래스를 참조하는 경우 모두 위험하다. 두번째 해결책은 Application context를 사용하는 것(역자 주 : 이게 제일 쉽다)이다. 이 context는 어플리케이션이 살아 있는 동안은 살아 있고 activity 라이프 싸이클에 종속되지 않는다. 만약 context를 필요로하는 객체가 오랫동안 살아 있어야 한다면 application context를 기억하라. Application context는 Context.getApplicationContext() 나 Activity.getApplication() 을 통해 쉽게 얻을 수 있다.

정리하면, context 관련 메모리 누수를 방지하기 위해 아래와 같은 내용을 기억해야한다.


  • 오랫동안 context-activity를 참조하는 것을 피하라. (activity에 대한 참조는 actiivty 자체의 라이프 싸이클과 같아야 한다)
  • context-activity보다 context-application를 사용하라.
  • 라이프 싸이클을 직접 관리하지 않을 꺼라면 activity에서 non-static inner클래스 사용을 피하고 static inner 클래스를 사용하고 activity내부에서 약한 참조(weak reference)를 만들어라. 이 해결책은 outer클래스에 대한 WeakReference 를 갖는 static inner 클래스를 사용한는 것이다. (ViewRoot 와 인스턴스를 위한 W inner클래스에서 구현)