안드로이드 프레임워크·시스템 학습 노트 목차

메모리·ANR·성능 — 누수·프리징·렉을 잡는다

앱이 느려지고, 멈추고(ANR), 메모리 부족으로 죽는 문제는 코드가 틀려서가 아니라 자원을 잘못 다뤄서 생긴다. 안드로이드는 제한된 메모리·단일 UI 스레드 위에서 돌기에 — 메모리 누수·메인 스레드 블로킹·과한 렌더링이 사용자 경험을 망친다. 이 장은 증상에서 원인까지 그 셋을 다룬다. (출처: Android — Manage memory / ANRs / App performance.)

메모리와 GC — ART의 관리형 힙

안드로이드 앱은 **ART(Android Runtime)**의 관리형 힙 위에서 돌고, GC가 도달 불가능한 객체를 회수한다. 문제는 — "논리적으로 끝났는데 누군가 참조를 붙잡고 있어 GC가 못 지우는" 객체, 즉 메모리 누수다. 누수가 쌓이면 *OutOfMemoryError*로 앱이 죽거나 GC가 자주 돌아 *렉(jank)*이 생긴다.

메모리 누수 — 가장 흔한 범인은 Context

안드로이드 누수의 대부분수명이 짧은 Activity/View의 Context를, 수명이 긴 객체가 붙잡을 때 생긴다.

// ❌ 누수: 정적 필드가 Activity를 붙잡음 — Activity가 파괴돼도 GC 불가
object Cache { var context: Context? = null }       // Activity context 저장 = 누수
class MyActivity { init { Cache.context = this } }

// ✅ applicationContext 사용 (앱 수명 = 안전)
Cache.context = applicationContext
다이어그램 로딩 중…

전형적 누수 원천:

  • 정적/싱글톤이 Activity Context 보유 → *applicationContext*를 쓰거나 약한 참조(WeakReference).
  • 내부 클래스·익명 클래스가 바깥 Activity를 암묵 참조(Handler·콜백) → static + WeakReference생명주기에 맞춰 해제.
  • 리스너·콜백·Observer 등록 후 미해제onStop/onDestroyView/onCleared에서 해제([01]).
  • 생명주기를 벗어난 코루틴(GlobalScope) → viewModelScope/lifecycleScope([09]).
  • Bitmap·큰 객체를 오래 들고 있기 → 캐시 크기 제한.

LeakCanary — 디버그 빌드에 넣으면 누수를 자동 탐지하고 참조 체인(누가 붙잡는지)을 보여준다. 누수 디버깅의 사실상 표준 도구다.

ANR — 메인 스레드를 막으면 멈춘다

**ANR(Application Not Responding)**은 메인(UI) 스레드를 너무 오래 막을 때(입력 이벤트 약 5초 미처리 등) OS가 띄우는 "앱이 응답하지 않음" 다이얼로그다. 원인은 한 가지로 모인다 — 메인 스레드에서 무거운 일을 한 것.

  • 네트워크·DB·파일 I/O를 메인에서코루틴 withContext(IO)([andkt 07])나 *suspend(Room·Retrofit)*로.
  • 무거운 계산(파싱·정렬·이미지)을 메인에서 → *Dispatchers.Default*로.
  • 큰 Bitmap 디코딩·과한 레이아웃 → 백그라운드/최적화.
// ❌ 메인에서 블로킹 — ANR
fun onClick() { val data = api.fetchBlocking() }    // 메인 스레드 멈춤
// ✅ 코루틴으로 백그라운드
fun onClick() = viewModelScope.launch { val data = withContext(IO){ api.fetch() } }

StrictMode(디버그)는 메인 스레드의 디스크·네트워크 접근을 감지개발 중에 ANR 위험을 잡아준다.

성능 — 렉(jank)과 렌더링

화면은 초당 60~120프레임으로 그려져야 부드럽다 — 한 프레임(약 16ms@60Hz) 안에 렌더링을 못 끝내면 **렉(jank·프레임 드랍)**이 보인다.

  • Compose recomposition 최적화불필요한 recomposition을 줄인다([03·04]). *안정적(stable) 타입·remember·derivedStateOf·key*로 읽는 범위를 좁힌다. 람다·불안정 파라미터skippable을 깨면 과한 recomposition이 난다.
  • 리스트LazyColumn으로 보이는 항목만 구성, *key*로 항목 안정화.
  • 이미지적정 해상도로 다운샘플링·캐시(Coil 등). 큰 Bitmap은 메모리 폭탄.

시작 시간·Baseline Profile

*앱 시작(startup)*도 성능 지표다. Baseline Profile은 *자주 쓰는 코드 경로를 미리 컴파일(AOT)*하도록 힌트를 줘 시작·첫 렌더를 빠르게 한다. App Startup 라이브러리로 초기화 순서를 정리한다.

측정 도구 — 추측 말고 프로파일링

  • Android Studio ProfilerCPU·메모리·네트워크·에너지 실시간.
  • Memory Profiler / 힙 덤프 — 누수·할당 추적, LeakCanary.
  • Perfetto / System Trace프레임 드랍·메인 스레드 블로킹을 타임라인으로.
  • Macrobenchmark시작·스크롤 성능을 수치로 측정.

정리 — 자원을 생명주기에 맞춰 다룬다

핵심 — 메모리 누수는 짧은 수명의 Context를 긴 수명 객체가 붙잡을 때(applicationContext·약한 참조·등록 해제·생명주기 스코프로 방지, LeakCanary로 탐지), ANR은 메인 스레드 블로킹(I/O·계산은 코루틴 IO/Default로, StrictMode로 조기 발견), 렉은 프레임 예산 초과(불필요 recomposition·LazyColumn key·이미지 다운샘플), 시작은 Baseline Profile, 모든 건 추측 말고 프로파일링. 안드로이드의 성능은 *"자원을 생명주기와 스레드에 올바르게 배치"*하는 데서 결정된다.

한 줄 요약 — 메모리 누수=짧은 Context를 긴 객체가 붙잡음(static/inner class/미해제 리스너/GlobalScope) → applicationContext·WeakReference·등록 해제·생명주기 스코프, LeakCanary 탐지. ANR=메인 스레드 블로킹(I/O·계산) → 코루틴 IO/Default·StrictMode. 렉=프레임 예산 초과 → recomposition 최소화·LazyColumn key·이미지 다운샘플. 시작은 Baseline Profile. 추측 말고 프로파일링(Profiler·Perfetto).

(출처: Android — Manage your app's memory / ANRs / Performance & jank / Baseline Profiles / LeakCanary.)

빌드 — Gradle·빌드 변형·R8/난독화·App BundleCompose 레이아웃·Modifier — Column·Row·Box·체이닝