Android: App 프로파일링과 최적화



안드로이드는 방대하며 하나의 이슈에 대해서도 수많은 솔루션들이 있다. Stackoverflow에서는 많은 추천을 받은 답변이 Best solution으로 인정되는 경향이 있는데, 특히 다른 개발자들의 긍정적인 코멘트들이 달린 답변의 경우에는 더 그렇다. 그러나 사실 백그라운드에서 비동기로 실행되는 것과 관련되어 보고된 이슈들을 보면, 많은 추천을 받은 답변들을 좋은 답변이라고 하기에는 어렵다. 그 중엔 종종 앱의 성능과 안정성에 미치는 영향을 완전히 무시한 솔루션들이 있는데, 경우에 따라서 이 솔루션들은 단지 약간의 성능저하 정도만 발생시킬 수 있겠지만, 어떤 경우에는 앱이 충돌할 가능성도 있다.


Splash 화면의 예

참고: 다음은 단지 최적화되지 않은 코드의 예시일 뿐이며, splash 화면을 구현하는데에 있어서 추천하는 방법이 절대 아니다. splash 화면 구현을 위한 적절한 솔루션은 Ian Lake의 Pro-tip 게시물을 참고하길 바란다.

Splash 화면을 만든다고 가정해보자. 어떻게 만들지에 대한 아이디어는 있지만 그것이 과연 최적의 방법인지는 확신할 수 없다. 일단 최선의 방법을 찾기 위해 구글링을 해볼것이고, Stackoverflow나 블로그들에서 여러 솔루션들을 얻게 될 것이다. 예를 들자면 이런 답변말이다:

이러한 코드는 구글에서 바로 검색되는 일반적인 솔루션이다. 이 코드는 Handler를 인스턴스화하고 Runnable로 postDelayed를 호출하여 splash 화면을 구현한 Activity다. SPLASH_DISPLAY_LENGTH만큼의 딜레이가 지연되고 나면 Runnable이 실행될 것이고, main Activity가 실행되며 splash가 종료될 것이다.

나는 이 코드를 복사하여 내 코드에 붙여넣은 뒤 원하는대로 잘 작동하는 것을 확인하며 만족해할 것이다. 여기까지는 아주 좋다.


코드 분석

이 코드에는 몇가지 결함이 있다:

1. Splash 화면이 보여지는 동안 사용자가 연달아 화면 방향을 전환하면 이 화면은 무한정으로 보여지게 됨.
2. 메모리 누수

이 포스팅은 프로파일링 및 최적화에 관한 글이기 때문에 난 2번 문제에 포커싱을 맞출 것이다. 하지만 그 전에 왜 내가 메모리 누수까지 신경써야 하는지 짚고 넘어가야 한다. 네이티브 언어인 C++과 달리 Java는 메모리가 관리 되어지기 때문인데, 그렇다면 Java가 알아서 개발자를 위해 자동으로 관리되는데 왜 신경을 써야 할까? 그 이유는 바로 '개발자를 위해서 자동으로 관리되는 것'이 아니기 때문이다.


프로파일링과 코드 최적화

이러한 이슈는 종종 안드로이드 커뮤니티에 'Leaking an Activity(메모리 누수)'란 이름으로 언급된다. 정확히 메모리 누수가 무슨 뜻일까?

화면 전환과 같은 configuration change가 일어날 때 Android는 Activity를 종료하고 재 생성한다. 일반적으로 가비지 컬렉터는 오래된 Activity의 할당된 메모리들을 해제할 것이다.

여기서 메모리 누수란 Activity instance 밖에서도 살아있는 강한 참조 때문에 가비지 컬렉터가 할당된 메모리들을 해제할 수 없는 상황을 말한다. 모든 Android 앱에는 특정 양의 메모리가 할당되어 있다. 가비지 컬렉터가 사용되지 않는 메모리를 더 이상 확보할 수 없는 경우, 앱의 성능이 점차 감소하고 궁극적으로 OutOfMemory와 함께 앱이 충돌하게 된다.

앱의 메모리 누수를 판단하는 가장 빠른 방법은, Android Studio의 Memory 탭을 열어서 화면 방향 전환시에 발생하는 메모리 할당에 주의를 기울이는 것이다.
메모리의 할당이 점점 증가하기만 하고 감소되지 않으면, 메모리 누수가 발생하고 있는 것이다.

메모리 누수의 예

그렇다면 어떻게 해결해야할까?


1. 먼저 StartMainActivityRunnable라는 정적 내부 클래스를 생성한다. 여기서 'static'으로 선언한다는 것이 중요한데, 비정적 내부 클래스는 상위 클래스에게 암묵적인 참조를 가지고 있어서 이는 메모리 누수의 또 다른 원인이 되기도 하기 때문이다. 또는 정적인 내부 클래스 대신에 독립적인 Activity class로 생성하는 것도 좋은 방법이다.

2. StartMainActivityRunnable에 Activity를 WeakReference 멤버변수로 정의하고 생성자에서 인스턴스화 한다. 화면 방향 전환이 발생했다던가 하는 이유로 인해 StartMainActivityRunnable이 Activity가 destroyed 된 후에 실행 될 수 있기 때문에 약한 참조를 사용하는 것인데, 동시에 이 WeakReference를 사용한다는 것은 가비지 컬렉터가 할당된 메모리를 해제하는 것을 막지 않겠다는 것을 의미하기도 한다.

3. StartMainActivityRunnable를 구현할 때, main Activity를 실행하는 코드 이전에 Activity instance가 여전히 유효한지 꼭 체크해야 한다.

4. 그 다음, StartMainActivityRunnable을 사용하는 클라이언트 코드를 처리하기 위해 Handler를 멤버변수로 선언한다. 멤버변수로 선언할 경우 Android 생명주기에서 이 Handler를 제어할 수 있으므로 매우 중요한 작업이다.

5. 현재 Activity의 instance를 넘기면서 StartMainActivityRunnable의 새 인스턴스를 생성하여 mHandler의 postDelayed() 메소드를 호출한다.

6. 마지막으로 가장 중요한 작업인 onDestroy() 메소드를 오버라이딩하여

7. mHandler의 removeCallbacksAndMessages(null) 메소드를 호출한다. 이 메소드는 Activity가 종료된 후에도 대기중인 처리되지 않은 Runnable을 제거하게 된다.

8. 그리고 끝으로 가비지컬렉터가 속히 해제시킬 수 있도록 mHandler를 null로 설정해준다.


작업 완료 후 Memory 탭을 다시 확인해보면, 그래프가 변하지 않고 평면을 유지하는 것을 볼 수 있다. 


* 이 글은 Nimrod Dayan의 'Android App Profiling and Optimization' 글을 의역하였습니다.

댓글

이 블로그의 인기 게시물

5년차의 슬럼프

10대 여고생이 만난 프로그래밍 - 마이크로소프트웨어 31주년 컨퍼런스

About Me