Context and memory leaks in Android written by Juan Rinconada 의 블로그를 번역한 글 입니다.
안드로이드 개발자라면 Context가 무엇인가라는 질문에 부딪히게 된다. Toasts, Adapters, Intents, Inflaters, SharedPreferences, SystemServices 들을 다룰 때, 꼭 Context가 꼭 필요하다. 즉, 이런 객체를 다루는 모든 활동에서, Context를 인자로 넣어야하는 것이다.
때로는 인자를 넣는 법이 간단하다고 생각하지만 (Activity에서 this 하나면 해결) , Context를 사용하면서도 내가 이걸 왜 인자로 전달해야하는 지 궁금할 때가 있다.
Context란 어플리케이션 환경에 대한 인터페이스이다.
그래서... 그게 무슨 말인데..?
" Context는 어플리케이션 환경에 대한 인터페이스이다 " 라는 말은, Context라는 클래스가 어플리케이션 리소스와 시스템 서비스등에 접근할 수 있는 메소드들을 갖고 있다는 것이다.
또한, Activity, Service, Application 들은 Context라는 추상클래스를 상속받은 구현체들이다. 앱 내에는 여러개의 Activity가 존재할 수 있고, 따라서 Activity Context도 여러개 생길 수 있다. 하지만, 어플리케이션은 하나이므로 Activity Context와는 다르게 Application Context는 1개만 존재한다.
Context는 일반적으로 뷰(Toast, Adapter, Inflaters), 액티비티 실행(Intents), 시스템 서비스 접근(SharedPreferences, ContentProviders)등에 사용된다. 보다 더 공식적인 분류로 나눈다면 다음과 같다.
Context를 얻는 3가지 방법이 있다.
1. View, 2. Activity, 3. ContextWrapper
이 3개의 클래스에 Context를 제공하는 메소드들이 있다.
View 클래스에서 제공하는 getContext()
는 뷰에 있는 Activity의 Context를 가져다준다. Application Context가 아닌 Activity Context이기 때문에, 특정 Activity의 심미적 변화와 관련된 주제들을 다룬다. 한마디로 Activity Context는 뷰를 관리할 때 쓴다고 할 수 있다. 레이아웃을 inflate하고, 다이얼로그를 보여주는 등, 단기적 작업들에 쓰인다.
Activity는 Context를 상속받은 하위 클래스이다. 1에서 언급한 Activity Context와 같은 개념이며, this
라는 키워드로 접근하는 그것이 맞다.
Activity클래스엔 Application Context를 가져오는 getApplicationContext()
라는 메소드도 있다. Application Context는 백그라운드 작업 또는 데이터 액세스와 같이 Activity의 라이프사이클에 국한되지 않고 유지되어야 하는 작업 을 할 때 사용된다.
상속 트리의 중간 클래스로 getBaseContext()
라는 메소드가 있다. 하지만 대부분 사용되지 않고, 추천하지도 않는다.
Application | Activity | Service | ContentProvider | BroadcastReceiver | |
---|---|---|---|---|---|
Show a Dialog | X | O | X | X | X |
Start an Activity | X | O | X | X | X |
Layout Inflation | X | O | X | X | X |
Start a Service | O | O | O | O | O |
Bind to a Service | O | O | O | O | X |
Send a Broadcast | O | O | O | O | O |
Register BroadcastReceiver | O | O | O | O | X |
Load Resource Value | O | O | O | O | O |
표에 Context와 사용되는 유스케이스들을 정리해보았다. Start an Activity
를 보면, Application과 Service에 X표시가 된 걸 볼 수 있다. 이건 호출스택이 없기 때문이다. Layout Inflation
의 경우도 Activity Context를 써야하는 이유는, 특정 액티비티별로 뷰가 구성되어야 하기 때문이다.
이러한 Context들에 대해 제대로 알아야 하는 이유는, 예기치 않은 문제로부터 우리를 구원해 주는 키가 되기 때문이다. 원인 모를 크래쉬 같은 것들이 메모리 누출과 밀접하게 관련되어 있다.
메모리누수란, 더이상 필요하지 않은 리소스가 RAM에서 해제되지 않고 계속 남아있는 것을 말한다. 메모리가 누수되면, 애플리케이션에 할당된 메모리가 초과되어 크래쉬가 발생하게된다.
메모리 누수의 일반적인 원인은 Static 변수, Singleton 패턴, 백그라운드 작업 및 익명 Inner 클래스때문이다. 이를 감지하고 메모리누수를 막는 몇 가지 예를 살펴보자.
static View vista;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Variable estática con referencia al contexto de actividad
vista = new View(this);
}
위 예시에서 static 변수 vista는 Activity Context를 참조하고 있다.
이 경우 어플리케이션이 계속 실행중이라면 Activity가 onDestroy()
되어도, vista 변수에 해당 Activity Context에 대한 참조가 있으므로 메모리가 해제되지 않는다.
@Override
protected void onDestroy() {
super.onDestroy();
vista = null;
}
여기서 해결책은 onDestroy()
메서드 내에서 vista 변수를 null로 만드는 것이다.
이런 비슷한 문제가 발생하는 케이스가 싱글톤을 사용할 때 Activity Context를 참조하는 경우이다.
이 경우 Best Practice는 Application Context를 사용하는 것이다. 따라서 우리는 MVC, MVVM, MVP등의 적절한 아키텍쳐로 View와 분리를 하여 Activity Context에 대한 접근이 필요없게 해야한다.
끔찍한 내부클래스는 다른 클래스나 메서드 안에서 생성된 클래스이다. 예를 들어 Activity 안에 만들어진 내부클래스는 상위 Activity의 Activity Context를 참조한다.
static Object innerClass;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
class InnerClass {
// Referencia automática a la Activity
}
innerClass = new InnerClass();
}
결국 싱글턴과 같은맥락이라고 할 수 있겠다. 실제로 innerClass 변수는 정적이다. 동적이라면 문제될 게 없다
Object innerClass
(static 잘가)
비동기 작업은 Activity가 종료된 뒤에도 백그라운드에서 계속 실행되므로 Activity에 접근 할 수 있다. 예를 들어 이 코드에서 AsyncTask는 Activity 내부에 생성된다.
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... voids) {
try {
// Inner class con el contexto de la actividad
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
}.execute();
이 코드의 문제는, 위에서처럼 익명 클래스를 만들고 있다는 것이다. 만약 이 코드를 AsyncTask를 상속한 별도의 클래스에 넣는다면 문제는 해결된다.
new MyAsyncTask.execute();
스레드도 동일한 문제를 일으킬 수 있지만, Activity가 onDestroy()
될 때 스레드를 중단 하면 된다.
Thread thread;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
thread = new Thread() {
@Override
public void run() {
if (!isInterrupted()) {
// Referencia al contexto de la actividad
}
}
};
thread.start();
}
@Override
protected void onDestroy() {
super.onDestroy();
thread.interrupt();
}
메모리누수가 발생할 수 있는 또 다른 케이스는, 시스템 서비스의 이벤트를 처리하기 위해 Activity에 리스너가 등록된 경우이다.
SensorManager sensorManager;
Sensor sensor;
private void registrarSensor () {
sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);
}
위의 센서예제를 보자.
해결책은 간단하다. Activity가 종료될 때 등록을 취소하면 된다.
private void desregistrarSensor () {
if (sensorManager != null && sensor != null) {
sensorManager.unregisterListener(this, sensor);
}
}
Memory Profiler: 안드로이드 스튜디오에 있으며 가비지 컬렉션과 메모리 소비에 대한 정보를 표시하는 가장 간편한 도구이다.
Leak Canary: 이 라이브러리를 앱에 설치하면, 메모리 누수를 발생시키는 것들을 추적할 수 있다.
번역 원문: https://medium.com/swlh/context-and-memory-leaks-in-android-82a39ed33002