앱 메모리 관리

RAM(Random Access Memory)은 모든 소프트웨어 개발 환경에서 중요한 리소스이지만 실제 메모리의 제약이 많은 모바일 운영체제에서는 더욱 중요합니다. Android 런타임(ART)과 Dalvik 가상 머신에서 일상적인 가비지 컬렉션을 실행하지만, 그래도 앱에서 메모리를 할당하고 해제하는 시기와 위치를 무시할 수는 없습니다. 정적 멤버 변수에서 개체 참조를 유지하여 일반적으로 발생하는 메모리 누수를 계속 방지하고 수명 주기 콜백에서 정의된 적절한 시간에 모든 Reference 개체를 해제해야 합니다.

이 페이지에서는 사전에 앱 내 메모리 사용을 줄이는 방법을 설명합니다. Android 운영체제에서 메모리를 관리하는 방법을 자세히 알아보려면 Android 메모리 관리 개요를 참조하세요.

사용 가능한 메모리 및 메모리 사용 모니터링

앱에서 메모리 사용 문제를 해결하려면 먼저 문제를 찾아야 합니다. Android 스튜디오의 메모리 프로파일러를 사용하면 다음과 같은 방법으로 메모리 문제를 찾고 진단할 수 있습니다.

  1. 앱에서 시간 경과에 따라 메모리를 할당하는 방법을 확인합니다. 메모리 프로파일러에서는 앱에서 사용 중인 메모리의 양, 할당된 자바 개체의 수, 가비지 컬렉션이 발생하는 시기를 실시간 그래프로 보여 줍니다.
  2. 가비지 컬렉션 이벤트를 시작하고 앱이 실행되는 동안 자바 힙 스냅샷을 찍습니다.
  3. 앱의 메모리 할당을 기록한 다음 할당된 개체를 모두 검사하고, 각 할당의 스택 추적을 확인하며, Android 스튜디오 편집기에서 해당 코드로 이동합니다.

이벤트에 대한 응답으로 메모리 해제

Android 메모리 관리 개요에 설명된 대로 Android에서는 여러 가지 방법으로 앱에서 메모리를 회수하거나 필요한 경우 앱을 완전히 종료시켜 중요한 작업을 위한 메모리를 확보합니다. 시스템 메모리의 균형을 유지하고 시스템에서 앱 프로세스를 종료하지 않도록 하려면 Activity 클래스에서 ComponentCallbacks2 인터페이스를 구현하면 됩니다. 제공된 onTrimMemory() 콜백 메서드를 사용하면 앱이 포그라운드나 백그라운드에 있을 때 메모리 관련 이벤트를 수신 대기한 다음 시스템이 메모리를 회수해야 한다는 것을 표시하는 앱 수명 주기 또는 시스템 이벤트에 응답하여 개체를 해제할 수 있습니다.

예를 들면 다음과 같이 onTrimMemory() 콜백을 구현하여 다양한 메모리 관련 이벤트에 응답할 수 있습니다.

Kotlin

    import android.content.ComponentCallbacks2
    // Other import statements ...

    class MainActivity : AppCompatActivity(), ComponentCallbacks2 {

        // Other activity code ...

        /**
         * Release memory when the UI becomes hidden or when system resources become low.
         * @param level the memory-related event that was raised.
         */
        override fun onTrimMemory(level: Int) {

            // Determine which lifecycle or system event was raised.
            when (level) {

                ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> {
                    /*
                       Release any UI objects that currently hold memory.

                       The user interface has moved to the background.
                    */
                }

                ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE,
                ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
                ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> {
                    /*
                       Release any memory that your app doesn't need to run.

                       The device is running low on memory while the app is running.
                       The event raised indicates the severity of the memory-related event.
                       If the event is TRIM_MEMORY_RUNNING_CRITICAL, then the system will
                       begin killing background processes.
                    */
                }

                ComponentCallbacks2.TRIM_MEMORY_BACKGROUND,
                ComponentCallbacks2.TRIM_MEMORY_MODERATE,
                ComponentCallbacks2.TRIM_MEMORY_COMPLETE -> {
                    /*
                       Release as much memory as the process can.

                       The app is on the LRU list and the system is running low on memory.
                       The event raised indicates where the app sits within the LRU list.
                       If the event is TRIM_MEMORY_COMPLETE, the process will be one of
                       the first to be terminated.
                    */
                }

                else -> {
                    /*
                      Release any non-critical data structures.

                      The app received an unrecognized memory level value
                      from the system. Treat this as a generic low-memory message.
                    */
                }
            }
        }
    }
    

자바

    import android.content.ComponentCallbacks2;
    // Other import statements ...

    public class MainActivity extends AppCompatActivity
        implements ComponentCallbacks2 {

        // Other activity code ...

        /**
         * Release memory when the UI becomes hidden or when system resources become low.
         * @param level the memory-related event that was raised.
         */
        public void onTrimMemory(int level) {

            // Determine which lifecycle or system event was raised.
            switch (level) {

                case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:

                    /*
                       Release any UI objects that currently hold memory.

                       The user interface has moved to the background.
                    */

                    break;

                case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
                case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
                case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:

                    /*
                       Release any memory that your app doesn't need to run.

                       The device is running low on memory while the app is running.
                       The event raised indicates the severity of the memory-related event.
                       If the event is TRIM_MEMORY_RUNNING_CRITICAL, then the system will
                       begin killing background processes.
                    */

                    break;

                case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
                case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
                case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:

                    /*
                       Release as much memory as the process can.

                       The app is on the LRU list and the system is running low on memory.
                       The event raised indicates where the app sits within the LRU list.
                       If the event is TRIM_MEMORY_COMPLETE, the process will be one of
                       the first to be terminated.
                    */

                    break;

                default:
                    /*
                      Release any non-critical data structures.

                      The app received an unrecognized memory level value
                      from the system. Treat this as a generic low-memory message.
                    */
                    break;
            }
        }
    }
    

onTrimMemory() 콜백이 Android 4.0(API 레벨 14)에 추가되었습니다. 이전 버전에서는 TRIM_MEMORY_COMPLETE 이벤트와 거의 동등한 onLowMemory()를 사용할 수 있습니다.

사용해야 하는 메모리 양 확인

Android에서는 여러 프로세스가 실행될 수 있도록 각 앱에 할당되는 힙 크기를 엄격하게 제한합니다. 정확한 힙 크기 한도는 기기에서 전반적으로 사용할 수 있는 RAM의 양에 따라 기기마다 다릅니다. 앱에서 힙 용량에 도달한 후 메모리를 더 할당하려고 하면 시스템에서 OutOfMemoryError를 발생시킵니다.

메모리 부족을 방지하려면 시스템에 쿼리하여 현재 기기에서 사용할 수 있는 힙 공간의 양을 확인하세요. getMemoryInfo()를 호출하여 시스템에 이 수치를 쿼리할 수 있습니다. 그러면 ActivityManager.MemoryInfo 개체를 반환합니다. 이 개체는 사용 가능한 메모리, 총 메모리, 메모리 임계값(시스템에서 메모리를 종료하기 시작하는 메모리 수준)을 비롯한 기기의 현재 메모리 상태에 관한 정보를 제공합니다. ActivityManager.MemoryInfo 개체도 기기에 메모리가 부족한지 알려 주는 간단한 부울 lowMemory를 표시합니다.

다음 코드 스니펫은 애플리케이션에서 getMemoryInfo() 메서드를 사용하는 방법을 보여주는 예입니다.

Kotlin

    fun doSomethingMemoryIntensive() {

        // Before doing something that requires a lot of memory,
        // check to see whether the device is in a low memory state.
        if (!getAvailableMemory().lowMemory) {
            // Do memory intensive work ...
        }
    }

    // Get a MemoryInfo object for the device's current memory status.
    private fun getAvailableMemory(): ActivityManager.MemoryInfo {
        val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
        return ActivityManager.MemoryInfo().also { memoryInfo ->
            activityManager.getMemoryInfo(memoryInfo)
        }
    }
    

자바

    public void doSomethingMemoryIntensive() {

        // Before doing something that requires a lot of memory,
        // check to see whether the device is in a low memory state.
        ActivityManager.MemoryInfo memoryInfo = getAvailableMemory();

        if (!memoryInfo.lowMemory) {
            // Do memory intensive work ...
        }
    }

    // Get a MemoryInfo object for the device's current memory status.
    private ActivityManager.MemoryInfo getAvailableMemory() {
        ActivityManager activityManager = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE);
        ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
        activityManager.getMemoryInfo(memoryInfo);
        return memoryInfo;
    }
    

메모리 효율성이 높은 코드 구성 사용

일부 Android 기능, 자바 클래스, 코드 구성은 다른 구성에 비해 메모리를 더 많이 사용합니다. 코드에서 효율성이 높은 대안을 선택하면 앱에서 사용하는 메모리의 양을 최소화할 수 있습니다.

서비스를 드물게 사용

Android 앱에서 일으킬 수 있는 최악의 메모리 관리 실수 중 하나는 서비스가 필요 없을 때도 실행되도록 두는 것입니다. 앱에 백그라운드에서 작업을 수행하는 서비스가 필요하다면 작업을 실행해야 하는 경우가 아니면 실행 상태를 유지하지 마세요. 작업을 완료한 서비스는 반드시 중지해야 합니다. 그러지 않으면 의도치 않게 메모리 누수가 발생할 수 있습니다.

서비스를 시작하면 시스템에서는 항상 그 서비스의 프로세스를 실행 상태로 유지하려고 합니다. 이러한 동작은 서비스에서 사용하는 RAM을 다른 프로세스에서 사용할 수 없기 때문에 서비스 프로세스의 비용이 매우 높아집니다. 이로 인해 시스템에서 LRU 캐시에 유지할 수 있는 캐시된 프로세스의 수를 줄여서 앱 전환의 효율성이 저하됩니다. 심지어 메모리가 부족하고 시스템에서 현재 실행 중인 모든 서비스를 호스팅하기에 충분한 프로세스를 유지할 수 없게 되면 시스템의 처리 속도가 급격히 저하될 수 있습니다.

일반적으로 지속적인 서비스 사용은 사용 가능한 메모리를 끊임없이 요구하므로 사용하지 않아야 합니다. 대신 JobScheduler와 같은 대체 구현을 사용하는 것이 좋습니다. JobScheduler를 사용하여 백그라운드 프로세스를 예약하는 방법에 관한 자세한 내용은 백그라운드 최적화를 참조하세요.

서비스를 사용해야 하는 경우 서비스의 수명을 제한하는 최선의 방법은 IntentService를 사용하는 것입니다. 이 서비스는 시작한 인텐트 처리를 완료하는 즉시 자체 종료됩니다. 자세한 내용은 백그라운드 서비스 실행을 참조하세요.

최적화된 데이터 컨테이너 사용

프로그래밍 언어에서 제공하는 일부 클래스는 휴대기기에서 사용하도록 최적화되지 않았습니다. 예를 들어 일반 HashMap 구현에는 모든 매핑에 별도의 항목 개체가 필요하므로 메모리가 상당히 비효율적일 수 있습니다.

Android 프레임워크에는 SparseArray, SparseBooleanArray, LongSparseArray를 비롯하여 최적화된 여러 데이터 컨테이너가 포함되어 있습니다. 예를 들어 SparseArray 클래스는 시스템에서 키와 때로는 값(항목당 개체를 1~2개 생성)을 자동 변환할 필요가 없도록 하므로 더 효율적입니다.

필요한 경우 언제든지 매우 가벼운 데이터 구조를 위해 원시 배열로 전환할 수 있습니다.

코드 추상화에 주의

추상화를 사용하면 코드 유연성과 유지 관리를 향상할 수 있어 개발자들이 좋은 프로그래밍 관행으로서 추상화를 사용하는 경우가 많습니다. 그러나 추상화를 위해서는 실행해야 할 코드가 꽤 많이 필요하여 코드를 메모리에 매핑하는 데 더 많은 시간과 RAM이 필요하므로 상당한 비용이 듭니다. 따라서 추상화를 통한 혜택이 크지 않다면 사용하지 않는 것이 좋습니다.

직렬화된 데이터에 라이트 protobuf 사용

프로토콜 버퍼는 Google에서 구조화된 데이터 직렬화를 위해 설계한 언어 및 플랫폼 중립적인 확장 가능한 메커니즘으로, XML과 비슷하지만 더 작고 빠르며 단순합니다. 데이터에 protobuf를 사용하기로 한 경우 항상 클라이언트 측 코드에서 라이트 protobuf를 사용해야 합니다. 일반 protobuf는 지나치게 상세한 코드를 생성하므로 RAM 사용 증가, APK 크기의 현저한 증가, 실행 속도 저하 등 앱에 다양한 문제를 일으킬 수 있습니다.

자세한 내용은 protobuf 추가 정보에서 '라이트 버전' 섹션을 참조하세요.

메모리 변동 방지

앞서 언급한 바와 같이 가비지 컬렉션 이벤트는 대개 앱 성능에 영향을 주지 않습니다. 하지만 단기간에 많은 가비지 컬렉션 이벤트가 발생하면 프레임 시간이 빠르게 소모될 수 있습니다. 시스템에서 가비지 컬렉션에 소비하는 시간이 길어질수록 오디오 렌더링이나 스트리밍 같은 다른 작업을 수행하는 시간이 줄어듭니다.

메모리 변동으로 인해 수많은 가비지 컬렉션 이벤트가 발생하는 경우가 많습니다. 실제로 메모리 변동은 특정 기간 내에 발생하는 할당된 임시 개체의 수를 나타냅니다.

예를 들어 for 루프 내에 여러 임시 개체를 할당할 수 있습니다. 또는 뷰의 onDraw() 함수 내에 새로운 Paint 또는 Bitmap 개체를 만들 수도 있습니다. 두 경우 모두 앱에서 많은 개체를 빠르게 대량으로 만듭니다. 이로 인해 새로운 객체 영역에서 사용 가능한 모든 메모리가 빠르게 소모되어 가비지 컬렉션 이벤트를 발생시킬 수 있습니다.

물론 문제를 해결하기 전에 코드에서 메모리 변동이 높은 위치를 찾아야 합니다. 이를 위해 Android 스튜디오의 메모리 프로파일러를 사용해야 합니다.

코드에서 문제 영역을 확인한 후에는 성능이 중요한 영역 내에 할당 수를 줄여 보세요. 또한 내부 루프에서 개체를 이동하거나 Factory 기반 할당 구조로 개체를 이동하는 것이 좋습니다.

메모리를 많이 사용하는 리소스와 라이브러리 삭제

코드에 있는 일부 리소스와 라이브러리가 모르는 사이에 메모리를 빠르게 소모할 수 있습니다. 타사 라이브러리 또는 삽입된 리소스를 비롯하여 APK의 전체 크기는 앱에서 사용하는 메모리 양에 영향을 줄 수 있습니다. 코드에서 중복되거나 불필요하거나 지나치게 무거운 구성요소, 리소스 또는 라이브러리를 삭제하면 앱의 메모리 소비를 향상할 수 있습니다.

전체 APK 크기 줄이기

앱의 전체 크기를 줄이면 앱의 메모리 사용량을 크게 줄일 수 있습니다. 비트맵 크기, 리소스, 애니메이션 프레임, 타사 라이브러리는 모두 APK의 크기를 증가시킬 수 있습니다. Android 스튜디오와 Android SDK에서는 리소스와 외부 종속성의 크기를 줄이는 데 도움이 되는 여러 도구를 제공합니다. 이러한 도구는 R8 컴파일과 같은 최신 코드 축소 메서드를 지원합니다. Android 스튜디오 3.3 이하에서는 R8 컴파일 대신 ProGuard를 사용합니다.

전체 APK 크기를 줄이는 방법에 관한 자세한 내용은 앱 크기를 줄이는 방법에 관한 가이드를 참조하세요.

종속성 주입에 Dagger 2 사용

종속성 주입 프레임워크는 작성하는 코드를 단순화하고 테스트 및 기타 구성 변경에 유용한 적응형 환경을 제공합니다.

앱에 종속성 주입 프레임워크를 사용하려는 경우 Dagger 2를 사용해 보세요. Dagger에서는 앱 코드 검색에 리플렉션을 사용하지 않습니다. Dagger의 정적 컴파일 시간 구현은 불필요한 런타임 비용이나 메모리를 사용하지 않아도 Android 앱에서 사용될 수 있습니다.

반영을 사용하는 기타 종속성 주입 프레임워크는 코드에서 주석을 검색하여 프로세스를 초기화하는 경향이 있습니다. 이 프로세스에 CPU 주기와 RAM이 훨씬 더 많이 필요할 수 있으며 앱이 시작될 때 현저한 지연이 발생할 수 있습니다.

외부 라이브러리 사용 관련 주의사항

외부 라이브러리 코드는 모바일 환경을 위해 작성되지 않는 경우가 많으며, 모바일 클라이언트에서 작업할 때 사용하면 비효율적일 수 있습니다. 외부 라이브러리를 사용하기로 결정했다면 그 라이브러리를 휴대기기에 맞춰 최적화해야 할 수 있습니다. 이러한 작업은 미리 계획하고 코드 크기와 RAM이 차지하는 공간 측면에서 라이브러리를 분석한 후 사용을 결정하세요.

일부 모바일에 최적화된 라이브러리도 다른 구현으로 인해 문제를 일으키는 경우가 있습니다. 예를 들어 한 라이브러리에서는 라이트 protobuf를 사용하고 다른 라이브러리는 마이크로 protobuf를 사용하여, 그 결과 앱에 두 개의 다른 protobuf가 구현될 수 있습니다. 이러한 문제는 로깅, 분석, 이미지 로드 프레임워크, 캐싱의 다양한 구현 및 예기치 않은 많은 다른 구현으로 인해 발생할 수 있습니다.

ProGuard를 사용하면 적절한 플래그로 API와 리소스를 삭제할 수 있지만 라이브러리의 큰 내부 종속성은 삭제할 수 없습니다. 이러한 라이브러리에서 원하는 기능이 낮은 수준의 종속성을 요구할 수도 있습니다. 이 경우 라이브러리의 Activity 서브클래스를 사용할 때(종속성이 많은 경향이 있음), 라이브러리에서 리플렉션을 사용할 때(일반적이며 작동하려면 ProGuard를 수동으로 조정하느라 시간이 많이 필요함) 등 여러 경우에 특히 문제가 됩니다.

또한 수십 가지 기능 중 한두 가지만 사용하기 위해 공유 라이브러리를 사용하지 마세요. 사용하지 않을 수많은 코드와 오버헤드가 발생할 수 있습니다. 라이브러리 사용 여부를 고려할 때 필요한 내용과 확실히 일치하는 구현을 찾으세요. 그러지 않으면 나만의 구현을 만들 수도 있습니다.