이번 편의 목표: 안드로이드 17이 프로세스를 메모리 한도로 종료했는지 구분하고, 알림의 비트맵 한도를 넘기지 않습니다.

포그라운드 앱이 메모리를 제한 없이 늘리면 캐시된 다른 앱이 먼저 죽고, 다음 실행은 콜드 스타트가 됩니다. 안드로이드 17은 기기 RAM을 기준으로 앱 메모리 한도를 두고 넘는 프로세스를 종료합니다. 이때 ApplicationExitInfo.getDescription()의 설명에는 MemoryLimiter:AnonSwap이 들어갑니다. 로그에 이 문자열이 있으면 일반적인 메모리 부족과 따로 기록합니다.

한도에 닿는 순간의 힙을 남기려면 ProfilingManager에 이상 징후 트리거를 등록합니다.

Kotlin
val profilingManager = applicationContext
    .getSystemService(ProfilingManager::class.java)
val triggers = listOf(
    ProfilingTrigger.Builder(ProfilingTrigger.TRIGGER_TYPE_ANOMALY).build()
)
profilingManager.addProfilingTriggers(triggers)

이 등록은 한도를 없애지 않습니다. 죽은 뒤 원인을 볼 힙 덤프를 남기는 설정입니다. 스튜디오 Panda의 프로파일러에는 LeakCanary가 작업으로 들어가 있어, 같은 누수를 소스와 연결해 볼 수 있습니다. 릴리스 빌드는 R8 전체 모드로 쓰지 않는 코드와 이름을 줄여 바이트코드가 차지하는 메모리를 낮춥니다.

알림과 위젯은 별도 한도입니다. targetSdk 37부터 RemoteViews 안의 비트맵과 아이콘이 차지하는 메모리가 1.5 곱하기 화면 너비 곱하기 화면 높이 곱하기 4를 넘으면 IllegalArgumentException으로 프로세스가 죽습니다. URI를 넣어 기존 크기 제한을 피하던 경로도 막힙니다. 알림에는 화면 크기와 무관하게 작은 그림을 넣습니다.

ART에는 젊은 세대만 자주 거두는 가비지 컬렉션이 추가되었습니다. 이 개선은 Play 시스템 업데이트로 안드로이드 12 이상 기기에도 전달됩니다. 앱이 직접 호출하는 API는 아닙니다.

static final 필드도 targetSdk 37에서 더 이상 바꾸지 못합니다. 리플렉션으로 바꾸면 IllegalAccessException이 나고, JNI의 SetStatic 계열로 바꾸면 프로세스가 즉시 죽습니다. 테스트가 정적 최종 필드를 고쳐 상태를 주입하고 있다면 그 방식을 바꿉니다.

직접 해 보기

최근 종료 기록을 읽어 getDescription()에 MemoryLimiter:AnonSwap이 있는지 확인하는 디버그 화면을 하나 두세요. 없다면 한도로 죽은 기록이 없는 것입니다.

해설

ApplicationExitInfo는 직전 종료의 이유입니다. 설명 문자열이 비어 있으면 한도 종료가 아닙니다. 위젯의 IllegalArgumentException은 프로세스 한도와 다른 예외이므로 스택의 RemoteViews를 따로 봅니다.