앱 메모리 관리

이 페이지에서는 사전에 앱 내 메모리 사용량을 줄이는 방법을 설명합니다. Android 운영체제에서 메모리를 관리하는 방법에 관한 자세한 내용은 메모리 관리 개요를 참고하세요.

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

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

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

  • 앱에서 시간 경과에 따라 메모리를 할당하는 방법을 확인합니다. 메모리 프로파일러에서는 앱에서 사용 중인 메모리의 양, 할당된 Java 객체의 수, 가비지 컬렉션이 발생하는 시점을 실시간 그래프로 보여줍니다.
  • 가비지 컬렉션 이벤트를 시작하고 앱이 실행되는 동안 Java 힙 스냅샷을 찍습니다.
  • 앱의 메모리 할당을 기록한 다음 할당된 객체를 모두 검사하고, 각 할당의 스택 트레이스를 확인하며, 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 is raised.
     */
    override fun onTrimMemory(level: Int) {

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

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

                   The user interface moves to the background.
                */
            }

            ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE,
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> {
                /*
                   Release any memory 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
                   begins stopping 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 is one of the
                   first to be terminated.
                */
            }

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

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

Java

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 is raised.
     */
    public void onTrimMemory(int level) {

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

            case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:

                /*
                   Release any UI objects that currently hold memory.

                   The user interface moves 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 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
                   begins stopping 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 is one of the
                   first to be terminated.
                */

                break;

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

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

필요한 메모리 용량 확인

Android에서는 여러 개의 프로세스 실행을 허용하기 위해 각 앱에 할당된 힙 크기를 엄격하게 제한합니다. 정확한 힙 크기 제한은 기기에서 전체적으로 사용할 수 있는 RAM 크기를 기준으로 기기에 따라 다릅니다. 앱이 힙 용량에 도달하여 더 많은 메모리를 할당하려고 하면 시스템에서 OutOfMemoryError가 발생합니다.

메모리 부족을 방지하려면 시스템에 쿼리하여 현재 기기에서 사용할 수 있는 힙 공간의 양을 확인하세요. getMemoryInfo()를 호출하여 시스템에 이 값을 쿼리할 수 있습니다. 이 메서드는 사용 가능한 메모리, 총 메모리 및 메모리 기준점(시스템에서 프로세스를 중지하기 시작하는 메모리 수준)과 같은 기기의 현재 메모리 상태 정보를 제공하는 ActivityManager.MemoryInfo 객체를 반환합니다. 또한 ActivityManager.MemoryInfo 객체는 기기의 메모리가 부족한지 알려주는 간단한 부울인 lowMemory를 노출합니다.

다음 코드 스니펫 예는 앱에서 getMemoryInfo() 메서드를 사용하는 방법을 보여줍니다.

Kotlin

fun doSomethingMemoryIntensive() {

    // Before doing something that requires a lot of memory,
    // check 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)
    }
}

Java

public void doSomethingMemoryIntensive() {

    // Before doing something that requires a lot of memory,
    // check 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 기능, Java 클래스, 코드 구성은 다른 기능, 클래스, 구성에 비해 메모리를 더 많이 사용합니다. 코드에서 효율성이 높은 대안을 선택하면 앱에서 사용하는 메모리의 양을 최소화할 수 있습니다.

서비스를 드물게 사용

불필요하면 서비스를 실행 상태로 두지 않는 것이 좋습니다. 불필요한 서비스를 계속 실행 상태로 두는 것은 Android 앱에서 일으킬 수 있는 최악의 메모리 관리 실수 중 하나입니다. 앱이 백그라운드에서 작동하는 서비스가 필요한 경우 작업을 실행해야 하는 경우가 아니면 서비스를 실행 상태로 두지 마세요. 작업이 완료되면 서비스를 중지합니다. 그러지 않으면 메모리 누수가 발생할 수 있습니다.

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

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

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

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

Android 프레임워크에는 SparseArray, SparseBooleanArray, LongSparseArray를 비롯하여 여러 최적화된 데이터 컨테이너가 포함되어 있습니다. 예를 들어, SparseArray 클래스는 키와 때로는 값(항목당 한두 개의 객체 생성)을 시스템에서 오토박싱하지 않으므로 더 효율적입니다.

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

코드 추상화 관련 주의사항

개발자는 코드 유연성과 유지관리를 개선할 수 있어 추상화를 좋은 프로그래밍 관행으로 사용하는 경우가 많습니다. 그러나 추상화는 훨씬 더 많은 비용이 듭니다. 일반적으로 실행해야 하는 코드가 더 많이 필요해 코드를 메모리에 매핑하는 데 더 많은 시간과 RAM이 필요하기 때문입니다. 추상화가 크게 유용하지 않다면 사용하지 마세요.

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

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

자세한 내용은 protobuf 리드미를 참고하세요.

메모리 급변 방지

가비지 컬렉션 이벤트는 앱 성능에 영향을 주지 않습니다. 하지만 단기간에 많은 가비지 컬렉션 이벤트가 발생하면 가비지 컬렉터와 앱 스레드 사이에 필요한 상호작용으로 인해 배터리가 빠르게 소모되고 프레임을 설정하는 데 걸리는 시간이 미미하게나마 늘어날 수 있습니다. 시스템에서 가비지 컬렉션에 소비하는 시간이 길어질수록 배터리가 빠르게 소모됩니다.

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

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

문제를 해결하기 전에 메모리 프로파일러를 사용하여 코드에서 메모리 급변이 높은 위치를 찾으세요.

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

객체 풀이 사용 사례에 도움이 되는지 평가할 수도 있습니다. 객체 풀을 사용하면 더 이상 필요하지 않은 객체 인스턴스를 삭제하는 대신 풀로 해제합니다. 다음번에 이 유형의 객체 인스턴스가 필요할 때 이를 할당하는 대신 풀에서 가져올 수 있습니다.

성능을 철저히 평가하여 특정 상황에서 객체 풀이 적합한지 확인합니다. 객체 풀을 사용함으로써 성능이 더 악화되는 경우도 있습니다. 풀을 사용하면 할당이 방지되지만 그 밖의 오버헤드가 발생합니다. 예를 들어, 풀을 유지관리하려면 보통 동기화가 진행되는데, 이 과정에서 무시할 수 없는 오버헤드가 발생합니다. 또한 해제 중에 메모리 누수를 방지하기 위해 풀링된 객체 인스턴스를 지운 다음 획득 중에 인스턴스를 초기화하는 과정에서도 0이 아닌 오버헤드가 발생할 수 있습니다.

풀에 필요한 것보다 많은 객체 인스턴스를 보유하는 것도 가비지 컬렉션에 부담이 됩니다. 객체 풀은 가비지 컬렉션 호출 수를 줄이지만 라이브(연결 가능한) 바이트 수에 비례하므로 모든 호출에 필요한 작업량은 증가하게 됩니다.

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

코드 내의 일부 리소스와 라이브러리는 모르는 사이에 메모리를 소비할 수 있습니다. 서드 파티 라이브러리 또는 삽입된 리소스를 비롯하여 APK의 전체 크기는 앱에서 사용하는 메모리 양에 영향을 줄 수 있습니다. 코드에서 중복되거나 불필요하거나 지나치게 커진 구성요소 또는 리소스 및 라이브러리를 삭제하여 앱의 메모리 소비를 개선할 수 있습니다.

전체 APK 크기 줄이기

앱의 전체 크기를 줄여 앱의 메모리 사용량을 상당히 줄일 수 있습니다. 비트맵 크기, 리소스, 애니메이션 프레임, 서드 파티 라이브러리는 모두 앱 크기에 영향을 미칠 수 있습니다. Android 스튜디오와 Android SDK는 리소스 및 외부 종속 항목의 크기를 줄이는 데 도움이 되는 여러 도구를 제공합니다. 이러한 도구는 R8 컴파일과 같은 최신 코드 축소 메서드를 지원합니다.

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

종속 항목 삽입에 Hilt 또는 Dagger 2 사용

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

앱에서 종속 항목 삽입 프레임워크를 사용하려는 경우 Hilt 또는 Dagger를 사용하는 것이 좋습니다. Hilt는 Dagger를 기반으로 실행되는 Android용 종속 항목 삽입 라이브러리입니다. Dagger는 앱 코드를 검색하는 데 리플렉션을 사용하지 않습니다. 불필요한 런타임 비용이나 메모리 사용 없이 Android 앱에서 Dagger의 정적 컴파일 시간 구현을 사용할 수 있습니다.

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

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

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

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

ProGuard를 사용하면 적절한 플래그로 API와 리소스를 삭제할 수 있지만 라이브러리의 큰 내부 종속 항목은 삭제할 수 없습니다. 이러한 라이브러리에서 원하는 기능에는 하위 수준의 종속 항목이 필요할 수 있습니다. 이는 라이브러리의 Activity 서브클래스를 사용하거나(광범위한 종속 항목이 있을 수 있음) 라이브러리에서 리플렉션을 사용할 때(일반적이며 작동하려면 ProGuard를 수동으로 조절해야 함) 특히 문제가 됩니다.

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