[Android] Foreground app crash when allocating memory

부제: Android Lollipop Bug

게임 개발을 하다보면 보통 인 게임과 아웃 게임으로 나뉘어서 리소스를 제어할 때가 많다. 아웃 게임이라고 하면 게임 코어요소를 제외한 로비나 상점, 인벤토리 같은 것을 말하고, 인 게임은 그 외에 게임의 코어한 요소(전투같은..)를 다루게 된다.  그래서 가끔 아웃 게임의 리소스를 메모리에 잔뜩 들고 있는 상태에서 인 게임에 진입하는 경우 앱이 내려가는 경우가 있는데, 이러한 경우는 인 게임의 리소스를 새로 할당하는 단계에서 메모리가 모자라게 되는 경우 OS에서 앱을 background로 내려버리게 된다. 물론 아웃 게임의 리소스가 별로 크지 않거나, 인 게임의 리소스가 크지 않다면 해당 안되는 이야기이다.  

좀 더 복잡한 아웃 게임 요소 및 다양한 인 게임 모드가 있는 경우는 메모리 관계가 더 복잡해 질 수 있다.  예를 들어, 전투 모드들 사이를 돌아다니거나 반복적인 전투를 하다보면 메모리가 점점 쌓여서 앱이 갑자기 내려가기도 한다.  메모리 관리는 쉬운 얘기는 아니다.  초기 부터 게임의 특성에 맞게 잘 설계한다면 관리가 잘 될 수도 있지만, 라이브 서비스를 하면서 여러 업데이트를 통해 신경쓰지 못하는 사이에 메모리의 해제가 안되어 쌓이게 되는 경우도 비일비재하기 때문이다.

내가 최근에 메모리에 대해서 신경쓰기 시작한 이유는 특정 폰(os 5.1.1)에서 앱이 background로 내려가는 현상이 나타났기 때문이었다. 이 기기는 3GB의 물리적 메모리를 가지고 있었기 때문에 얼핏 보면 메모리 때문이라고 생각하지 못할 수 있었다.  2GB폰이라고 하면 무시하고 지나쳤으리라 생각된다.
일단 logCat을 통해 어떤 로그들이 남는지 살펴 보았다.  (혹시 시스템 메세지 같이 의미있는 로그를 찍어줄까 해서..)

06-05 11:59:29.311 18657-18719/? E/Unity: Could not allocate memory: System out of memory! 
Trying to allocate: 4194304B with 16 alignment. MemoryLabel: Texture                                          
Allocation happend at: Line:559 in ./Runtime/Utilities/dynamic_array.h                                          
Memory overview
.........

의미있는 로그가 나왔다.  “System out of memory”.  메모리가 부족하다는 얘기였고, 메모리가 원인이었다!!! 마지막으로 4194304B (4MB)를 할당하다가 죽었다니.. 왜일까.

그래서 터미널을 통해 adb 쉘 명령어를 날려 메모리 덤프를 떠보았다.

>adb shell dumpsys meminfo
  
Total PSS by process:

  1355559 (   4444) kB: ***.***.***.*** (pid 30724 / activities)
   122903 (  22788) kB: com.google.android.gms (pid 2140)
    94344 (  17012) kB: system (pid 1004)
    89847 (  37548) kB: com.sec.android.app.launcher (pid 1723 / activities)
    79830 (  26512) kB: com.sec.android.inputmethod (pid 2006)
.......

위와 같이 가장 많이 메모리를 차지하는 process부터 차례로 나열되어 있다. 현재 foreground에서 플레이 중인 앱이 ***.***.***.*** 이고, 대력 1.35GB를 차지하고 있는 것을 알 수 있다.

가장 밑에는 Total과 Free 메모리상황을 볼 수 있다.

Total RAM: 2844912 kB (status normal)
Free RAM: 495705 kB (244913 cached pss + 180428 cached kernel + 70364 free)
Used RAM: 2173166 kB (2020006 used pss + 153160 kernel)
Lost RAM: 176041 kB 

Free RAM이 495MB나 있었는데, 왜 out of memory로 인해 앱이 다운됐을까.
Android의 메모리 관리에 대해 정확히 알 필요가 있었다. 

Android 메모리에 대해서 Googling을 해보면 아래의 글을 찾을 수 있다. Overview of memory management (link)에서 현재 이슈와 관련된 내용은 아래 두개의 섹션에서 잘 설명되어 있었다.


Restrict app memory 섹션

link

(해석) 기능상 멀티 태스킹 환경을 유지하기 위해서, Android는 각 앱에 대해서 heap 사이즈 사용량에 대한 강력한 제한을 걸어두었다.  정확한 heap 사이즈 허용량은 디바이스가 얼마 만큼의 메모리를 가지고 있느냐에 따라 다르다.  만약 너의 앱이 heap 허용치에 도달했고, 더 많은 메모리를 할당하려고 한다면 OutOfMemoryError를 받을 수 있다.
경우에 따라서, 너의 앱은 현재 디바이스에서 얼마의 heap 공간이 허용되는지 알고 싶을 수 있다.  예를 들어, 얼마의 데이터를 캐쉬할 수 있는지 같은 경우말이다.  이러한 경우에 getMemoryClass()를 호출하여 시스템에 메모리 허용치에 대해 쿼리 할 수 있다.  이 메서드는 해당 앱의 heap이 사용할 수 있는 용량을 Megabyte로 리턴해준다.


Switch apps 섹션도 보자.

link

(해석)유저가 앱들을 이것저것 사용중일 때, 유저에게 보이지 않는 앱들 (foreground가 아닌 앱)은 LRU 캐쉬에서 유지된다.  예를 들어, 유저가 앱을 최초로 실행하면, 프로세스가 생성되지만, 유저가 그 앱을 떠날 때는 그 프로세스는 종료되지 않는다.  시스템은 그 프로세스를 캐쉬하여 만약 유저가 그 앱으로 돌아올때, 캐쉬된 프로세스를 재사용함으로써 앱의 스위칭을 빠르게 한다.  
만약 너의 앱이 캐쉬되어 있고 현재 필요하지 않지만 메모리를 차지하고 있다면, 너의 앱은 시스템의 전체 성능에 영향을 주게 된다.  시스템의 메모리가 낮아 지게 되면, 시스템은 가장 늦게 사용된 프로세스부터 죽이기 시작한다.  시스템은 또한 어떤 프로세스가 많은 메모리를 들고 있다면 그것부터 종료시킬 수도 있다.


위에 내용을 정리해보면 대략 이렇다. 

  • 앱마다 heap 에 대한 허용치가 있고, 그 허용치 이상을 요청하게 되면 앱은 시스템으로 부터 경고 메세지를 받을 수 있고, background로 내려갈 수 있다.  
  • 시스템은 foreground 앱이 메모리가 더 필요한 경우에는 background 앱 프로세스를 종료하여 메모리를 확보한다.

그렇다면 내가 겪고 있는 상황이 어떤 것일까.  두 가지 프로그래밍 방식으로 메모리의 상태를 확인할 수 있었다.

  1. ComponentCallbacks2 interface를 implement하여  onTrimMemory() callback을 받는 방식 (link)
    • onTrimMessage() callback을 통해 앱의 메모리 상태가 low인지 critical한 상태인지 받을 수 있다.  예제코드는 링크에 잘 나와있다.
  2. API호출을 통해 메모리 사용 수치 정보를 얻는 방식 (link)
    • 필요할 때 API호출을 통해 해당 메모리의 상태 정보를 얻을 수 있다.  getMemoryInfo의 경우 lowMemory 필드에서 현재 low 메모리 상태인지 여부를 boolean타입으로 알 수 있다.

위의 두 방식으로 테스트를 해 본 결과, 앱이 내려가기 전에 onTrimMemory() callback도 받지 못했고, getAvailabeMemory()에서 얻은 memoryInfo또한 lowMemory가 아니였다. (.. OTL …)

API를 통해 얻은 값이 정상이라면, 혹시 OS에 문제가 있는 것은 아닐까하는 의심을 해보았다. Google에서 “Android lollipop memory”라고 검색을 해보았다.

Memory leak이라는 것이 눈에 띄였다.  lollipop memory leak으로 좀 더 검색을 해보면, 5.0에서 메모리 릭이 있었고, 관련한 증상으로는 foreground의 앱에서 메모리 할당시 여유 메모리가 있음에도 불구하고 종료가 된다는 것이었다. 내가 겪고 있던 현상과 거의 일치해 보였다. 이상한 것은 android 각 버젼에 대한 release note나 bug fix 문서를 찾기 어려웠다.  각종 인터넷 기사와 wiki에서만 자료를 찾을 수가 있었는데, 열심히 검색해도 찾기 어려운건 어딘가 있더라도 문제긴 문제인것 같다. 

아무튼 android lollipop에 대한 wiki를 보던 중에 5.1.1에 대한 간단한 release note를 볼 수 있었는데, ‘RAM 누수 버그 수정’이라는 것이었고, 5.1.1 R3 (LMY48B) 부터 적용된다는 것이었다.

< wiki link >

내가 가진 디바이스의 정보를 보니 5.1.1 (LMY47X)였고, 수정된 버젼인 LMY48B 이전의 빌드였다. 

< https://source.android.com/setup/start/build-numbers>

이제 좀 이해가 된다.

즉, Android Lollipop 5.1.1 R3 (LMY48B) 이전 버전에서는 foreground에 있는 앱에 메모리 할당시에 버그가 있다는 것이고, 그것은 getAvailabeMemory API나 onTrimMemory 콜백으로도 확인이 되지 않는다. 다행히 이후 OS 버젼에서 테스트해보니 위와 같은 버그는 더 이상 나타나지 않았다.  무수한 테스트와 검색을 통해 많은 시간을 소모했지만 다행히 실마리를 잡아 원인을 파악한 것이 그나마 다행인 것 같다. 

끝.

Leave a Reply

Your email address will not be published. Required fields are marked *