말 그대로! 어플리케이션이 응답하지 않는다는 오류이다. 우리는 은근히 아래와 같은 에러 메세지를 많이 봤다.
응답하지 않는다는 말에서 유추해보자. 어플이 응답하지 않는 다는 말이 무슨 뜻일까?
안드로이드 앱은 사용자에게 보여지는 UI 작업을 처리하기 위한 쓰레드를 UI 쓰레드라고 하고, 이를 메인 쓰레드라고 한다. 앱이 원활하게 동작되기 위해서는 UI 쓰레드가 블로킹되어서는 안된다. UI 쓰레드가 오랫동안 블로킹되면, 사용자의 키 입력이나 터치 입력을 처리하지 못하게 된다. 이를 '응답하지 않는다'고 표현하는 것이다.
때문에, ANR 이 발생했다고 함은 'UI 쓰레드가 오랫동안 블로킹되었다'는 뜻이다.
com.android.server.am.ActivityManagerService
을 참고해보면 아래와 같이 ANR 관련한 내용을 확인해볼 수 있다. ActivityManagerService
는 SYSTEM_SERVER
프로세스에서 실행된다.
// How long we allow a receiver to run before giving up on it.
static final int BROADCAST_FG_TIMEOUT = 10*1000;
static final int BROADCAST_BG_TIMEOUT = 60*1000;
// How long we wait until we timeout on key dispatching.
static final int KEY_DISPATCHING_TIMEOUT = 5*1000;
/**
* ICS 이하 버전
*/
// How long we allow a receiver to run before giving up on it.
static final int BROADCAST_TIMEOUT = 10*1000;
// How long we wait for a service to finish executing.
static final int SERVICE_TIMEOUT = 20*1000;
// How long we wait until we timeout on key dispatching.
static final int KEY_DISPATCHING_TIMEOUT = 5*1000;
참고로 타임아웃은 안드로이드 버전별로 상이하다.
메인 쓰레드를 어디선가 점유하고 있다면 사용자의 키 입력 이벤트 (메뉴키, 뒤로가기 키, 볼륨키 등) 를 전달하지 못한다. 만약 일정 시간 (타임아웃) 이상 이벤트를 전달 못한 경우, ANR 이 발생한다. 위에 정의되어 있는 상수들 중 아래에 해당한다. 5초 이상 키 이벤트가 지연될 경우 ANR 을 발생하도록 한다.
static final int KEY_DISPATCHING_TIMEOUT = 5 * 1000;
홈 키와 전원 키는 워낙 중요한 키 이므로, 앱과 별개로 동작하며 ANR 발생과 무관하다.
키 이벤트 말고 사용자의 터치 이벤트는 처리 방식이 조금 다르다. 터치 이벤트도 메인 쓰레드를 어디선가 점유하고 있다면 대기하는 것은 동일하지만, 타임아웃 시에 ANR 을 발생하진 않는다. 하지만 그 다음으로 이어서 들어오는 두 번째 터치 이벤트까지 타임아웃된다면 ANR 을 발생한다. 이 역시 타임아웃 기점을 5초로 잡는다.
특정 메세지 처리 각각이 5초가 넘더라도, 그 사이에 터치 이벤트가 발생하지 않는다면 ANR 이 발생하지 않는다. 아래와 같은 상황을 보자.
for (i in 0..4) {
// Blocking UI Thread for 2000ms
}
for
문이 0부터 4까지 돌아가는데 각각 루프에서 2초동안 메인 쓰레드를 블로킹한다고 해보자. 총 5개의 메세지를 처리하는 것이고, 따라서 총합 10초가 소요되는 작업이다. 이 경우 그대로 간다면 문제가 발생하지 않는다.
하지만, 메세지를 처리하던 도중 사용자가 화면을 두 번 이상 터치하게 되면 ANR 이 발생한다.
for
문에 의해 쌓여있는 메세지를 먼저 처리하느라 터치 이벤트에 대한 처리가 지연되기 때문이다.
위에서 아래와 같은 타임아웃 상수를 잠시 살펴봤다. 상수명을 살펴보니 BroadcastReceiver
에 대한 이야기인듯 하다. 실제로 이들은 Foreground Broadcast, Background Broadcast 에 대한 ANR 타임아웃을 정의해놓은 것이다.
static final int BROADCAST_FG_TIMEOUT = 10 * 1000;
static final int BROADCAST_BG_TIMEOUT = 60 * 1000;
위에서 말하는 타임아웃은 즉, 브로드캐스트 리시버의 onReceive()
가 얼마나 작업을 수행하고 있냐에 대한 타임아웃이다.
Foreground Broadcast 의 경우에는 메세지 처리 우선순위가 더 높지만 ANR 이 발생하는 타임아웃 제한은 더 빡세다. 왜냐하면 포그라운드에서 동작하는만큼 사용자에게 영향을 끼칠 가능성이 더 높기 때문이다. 우선순위를 높게 책정한 이유 역시 ANR 타임아웃이 작기 때문에 빨리 처리해야 하기 때문이다.
// Broadcast Receiver
class ANRTestReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Log.d(TAG, "Start Blocking")
// onReceive() 에서 50초 블로킹
sleep(50000)
Log.d(TAG, "Stop Blocking")
}
}
// Foregroun broadcast
val intent = Intent(this, ANRTestReceiver::class.java)
intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
sendBroadcast(intent)
// Background broadcast
val intent = Intent(this, ANRTestReceiver::class.java)
sendBroadcast(intent)
위 코드를 살펴보자. 위 예제 코드에선 이미지를 다운로드하는 작업을 요청하는 브로드캐스트를 송신한다. 그리고 수신부에서는 이를 11초뒤에 받도록 구현해두었다. 이 경우 ANR 이 발생한다. 포그라운드 브로드캐스트 타임아웃이 10초로 정의되어 있기 때문이다.
Receiver 역시 액티비티와 마찬가지로 메인 쓰레드를 사용하기 때문에, Receiver 에서 오래 걸리는 작업을 수행할 경우 ANR 발생의 원인이 된다. 따라서 Receiver 에서는 짧은 시간 안에 완수할 수 있는 작업만 정의하고, 오래걸리는 작업의 경우 별도의 쓰레드를 두거나 서비스를 사용해야 한다.
아래와 같이 서비스에서 20초 이상 작업하는경우 역시 ANR 이 발생하게 된다. 서비스도 결국 기본적으로는 메인 쓰레드 위에서 동작하기 때문이다.
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d(TAG, "Start Blocking")
sleep(20000)
Log.d(TAG, "Stop Blocking")
return START_STICKY
}
BroadcastReceiver
혹은 Service
둘다 액티비티가 화면에 떠있는 상태 (Foreground 상태) 를 고려하여 구현하는 편이 낫다. 포그라운드 상태인 경우 언제든 사용자에 의한 터치 이벤트가 발생해도 이상하지 않기 때문이다. 결론적으로 BroadcastReceiver
의 경우에는 오래 걸리는 작업이 있다면 Service
로 넘겨 실행해야 하고, Service
에서도 역시 20초 이상 걸리는 작업인 경우 별도의 백그라운드 쓰레드를 이용해야 한다.
(당연하게도) 메인 쓰레드에 대해 조심해야 한다. 네트워킹, DB 트랜잭션 (I/O 작업 등) 등 많은 시간동안 수행해야 하는 작업의 경우 ANR 발생 가능성이 매우 높기 때문에 별도의 쓰레드를 사용하거나 비동기 데이터 스트림 (RxJava 등) 을 활요해야 한다.
https://ch4njun.tistory.com/189
https://developer.android.com/topic/performance/vitals/anr
https://inomp.tistory.com/17