메인 스레드가 막혀 시스템이 응답 없음으로 판정하는 것.
- 입력 이벤트 처리 — 5초
- 포그라운드 서비스 시작 — 5초 (startForeground 미호출)
- BroadcastReceiver — 10초 (포그라운드) / 60초 (백그라운드)
- ContentProvider — 10초
메인 스레드가 하는 일
- 화면 그리기 (16.6ms 마다 한 프레임 — 60Hz 기준)
- 터치 이벤트 처리
- 생명주기 콜백
여기서 100ms 를 쓰면 6프레임이 날아간다 (눈에 보인다) 5초를 쓰면 ANR 다
전형적인 원인
-
메인에서 네트워크 · 파일 · DB 접근
-
메인에서 큰 JSON 파싱 · 이미지 디코딩 · 암호화
-
락 경합 (메인이 백그라운드 스레드가 쥔 락을 기다린다)
-
데드락
-
Binder 호출이 늦는 경우 (다른 앱/시스템 프로세스 대기)
-
"IO 를 안 했는데 ANR" 이면 락 경합을 의심한다
StrictMode로 개발 중에 잡는다
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
.detectDiskReads().detectDiskWrites().detectNetwork()
.penaltyLog()
.build())
StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder()
.detectLeakedClosableObjects().detectActivityLeaks()
.penaltyLog().build())
}
메인 스레드에서 디스크·네트워크를 건드리면 즉시 로그를 남긴다 penaltyDeath() 로 하면 크래시시켜 강제로 고치게 만들 수도 있다
원인을 찾는 법
- /data/anr/traces.txt — 모든 스레드의 스택이 찍힌다
- main 스레드가 무엇을 하고 있었는지가 첫 번째 단서
- adb bugreport
- Play Console → Android vitals → ANR 비율
- "ANR 발생률 0.47% 초과" 면 스토어 노출이 불이익을 받는다
SharedPreferences가 흔한 함정
prefs.edit().putString(k, v).apply() // 비동기. 하지만 완전히 공짜는 아니다
prefs.edit().putString(k, v).commit() // 동기 디스크 쓰기 — 메인에서 부르면 위험
apply() 도 첫 getSharedPreferences 로드는 동기다 그리고 Activity 의 onPause/onStop 은 apply() 대기열이 비워질 때까지 기다린다
-
큰 prefs 가 화면 전환을 막을 수 있다
-
DataStore 로 옮기면 이 문제가 사라진다
면접 함정
- ❌ "5초 안에만 끝나면 된다" → 16ms를 넘기면 이미 프레임이 밀린다. ANR은 최악의 경계일 뿐이다.
- ❌ "코루틴을 쓰면 안전하다" →
Dispatchers.Main에서 블로킹 코드를 돌리면 똑같다.
락 경합으로 생기는 ANR
// ❌ 메인이 IO 스레드가 쥔 락을 기다린다
synchronized(lock) { readFromDisk() } // IO 스레드
synchronized(lock) { getCachedValue() } // 메인 스레드 → 여기서 멈춘다
traces.txt 에서 이렇게 보인다
- "main" ... Blocked on lock held by thread 21
- 21번 스레드가 무엇을 하고 있었는지를 함께 본다
프레임 예산
- 60Hz — → 16.6ms
- 90Hz — → 11.1ms
- 120Hz → 8.3ms
이 안에 '측정 + 배치 + 그리기' 를 끝내야 한다 넘으면 그 프레임이 버려지고 화면이 끊긴다 (잰크)
고주사율 기기가 흔해지며 예산이 절반으로 줄었다
무엇이 프레임을 먹는가
-
리컴포지션 폭주 (불안정한 파라미터)
-
목록 항목에서 이미지 디코딩
-
메인에서의 정렬·필터링
-
과도한 오버드로 (겹친 배경을 여러 번 칠한다)
-
레이아웃 중첩이 깊다
-
Perfetto 로 프레임 단위 추적을 뜨면 어느 단계가 오래 걸렸는지 정확히 보인다
백그라운드 ANR도 있다
-
포그라운드 서비스가 5초 안에 startForeground 를 안 부른다
-
BroadcastReceiver.onReceive 가 제한을 넘는다
-
ContentProvider 가 10초를 넘는다
-
화면이 없으니 사용자는 모르지만 vitals 에는 집계된다