ANR

Android 앱의 UI 스레드가 너무 오랫동안 차단되면 'ANR(애플리케이션 응답 없음)' 오류가 트리거됩니다. 앱이 포그라운드에 있으면 그림 1에서와 같이 시스템에서 사용자에게 대화상자를 표시합니다. 사용자가 ANR 대화상자에서 앱을 강제 종료할 수 있습니다.

그림 1. 사용자에게 표시되는 ANR 대화상자

그림 1. 사용자에게 표시되는 ANR 대화상자

ANR은 UI 업데이트를 담당하는 앱의 기본 스레드가 사용자 입력 이벤트 또는 그리기를 처리하지 못하여 사용자 불만을 초래하므로 문제가 됩니다. 앱의 기본 스레드에 관한 자세한 내용은 프로세스 및 스레드를 참고하세요.

다음 조건 중 하나가 발생하면 앱과 관련한 ANR이 트리거됩니다.

  • 입력 전달 타임아웃: 앱이 입력 이벤트(예: 키 누름 또는 화면 터치)에 5초 이내에 응답하지 않은 경우
  • 서비스 실행: 앱에서 선언한 서비스가 몇 초 이내에 Service.onCreate()Service.onStartCommand()/Service.onBind() 실행을 완료할 수 없는 경우
  • Service.startForeground()가 호출되지 않음: 앱이 Context.startForegroundService()를 사용하여 포그라운드에서 새 서비스를 시작했지만 서비스가 5초 내에 startForeground()를 호출하지 않은 경우
  • 인텐트 브로드캐스트: BroadcastReceiver가 설정된 시간 내에 실행을 완료하지 못한 경우. 앱에 포그라운드 활동이 있는 경우 이 제한 시간은 5초입니다.
  • JobScheduler 상호작용: JobService가 몇 초 이내에 JobService.onStartJob() 또는 JobService.onStopJob()에서 반환되지 않거나 사용자 시작 작업이 시작되고 JobService.onStartJob()이 호출된 후 몇 초 이내에 앱이 JobService.setNotification()을 호출하지 않는 경우. Android 13 및 이전 버전을 타겟팅하는 앱의 경우 ANR이 음소거되고 앱에 보고되지 않습니다. Android 14 및 이후 버전을 타겟팅하는 앱의 경우 ANR이 명시적이고 앱에 보고됩니다.

앱에 ANR이 발생하면 이 자료의 안내를 사용하여 문제를 진단하고 해결할 수 있습니다.

문제 감지

이미 앱을 게시했다면 Android vitals를 사용하여 앱의 ANR에 관한 정보를 확인할 수 있습니다. 다른 도구를 사용하여 필드에서 ANR을 감지할 수 있지만 서드 파티 도구는 Android vitals와 달리 이전 버전의 Android(Android 10 이하)에서 ANR을 보고할 수 없습니다.

Android vitals

Android vitals를 사용하면 앱의 ANR 발생률을 모니터링하고 개선할 수 있습니다. Android vitals는 다음과 같은 ANR 발생률을 측정합니다.

  • ANR 발생률: 모든 유형의 ANR을 경험한 일일 활성 사용자의 비율입니다.
  • 사용자 인식 ANR 발생률: 한 번 이상 사용자 인식 ANR을 경험한 일일 활성 사용자의 비율입니다. 현재 Input dispatching timed out 유형의 ANR만 사용자 인식 ANR로 간주됩니다.
  • 다중 ANR 발생률: ANR을 두 번 이상 경험한 일일 사용자의 비율입니다.

일일 활성 사용자는 한 기기에서 하루에 여러 세션에 걸쳐 앱을 사용하는 순 사용자입니다. 사용자가 하루에 두 대 이상의 기기에서 앱을 사용하는 경우 각 기기는 해당 날짜의 활성 사용자로 계산됩니다. 여러 사용자가 하루 동안 동일한 기기를 사용하는 경우 활성 사용자 한 명으로 계산됩니다.

사용자 인식 ANR 발생률은 핵심 vitals에 속하는 요소로, Google Play에서 앱의 검색 가능 여부에 영향을 미칩니다. 사용자 인식 ANR 발생률에 집계되는 ANR은 항상 사용자가 앱을 사용할 때 발생하여 가장 큰 지장을 유발하므로 중요합니다.

Play에서는 이 측정항목에 대해 비정상적인 동작 기준 두 가지를 정의했습니다.

  • 전반적 비정상적인 동작 기준: 일일 활성 사용자의 0.47% 이상이 모든 기기 모델에서 사용자 인식 ANR을 경험합니다.
  • 기기별 비정상적인 동작 기준: 일일 사용자의 8% 이상이 단일 기기 모델에서 사용자 인식 ANR을 경험합니다.

앱이 전반적 비정상적인 동작 기준을 초과하면 모든 기기에서 검색 가능성이 낮아질 수 있습니다. 앱이 일부 기기에서 기기별 비정상적인 동작 기준을 초과하면 해당 기기에서 검색 가능성이 낮아질 수 있으며 스토어 등록정보에 경고가 표시될 수도 있습니다.

앱에서 ANR이 과도하게 발생하면 Android vitals에서 Play Console을 통해 알림을 보낼 수 있습니다.

Google Play에서 Android vitals 데이터를 수집하는 방법에 관한 자세한 내용은 Play Console 문서를 참고하세요.

ANR 진단

ANR을 진단할 때 다음과 같은 일반 패턴을 찾아야 합니다.

  • 앱이 기본 스레드에서 I/O와 관련된 느린 작업을 실행 중입니다.
  • 앱이 기본 스레드에서 긴 계산을 실행 중입니다.
  • 기본 스레드에서 다른 프로세스에 관한 동기 바인더 호출을 실행 중이고 다른 프로세스가 반환하는 데 오랜 시간이 걸립니다.
  • 다른 스레드에서 발생하는 긴 작업을 위해 동기화된 블록을 대기하는 동안 기본 스레드가 차단되었습니다.
  • 기본 스레드가 프로세스에서 또는 바인더 호출을 통해 다른 스레드와 교착 상태에 있습니다. 기본 스레드가 긴 작업이 완료될 때까지 대기하는 것만이 아니라 교착 상태에 있습니다. 자세한 내용은 위키백과에서 교착 상태를 참고하세요.

다음 기법을 통해 ANR의 원인을 확인할 수 있습니다.

HealthStats

HealthStats는 총 사용자 및 시스템 시간, CPU 시간, 네트워크, 무선 통계, 화면 켜짐/꺼짐 시간, 깨우기 알람을 캡처하여 애플리케이션의 상태에 관한 측정항목을 제공합니다. 이렇게 하면 전체 CPU 사용량과 배터리 소모를 측정하는 데 도움이 됩니다.

디버그

Debug는 앱의 버벅거림 및 지연을 식별하는 추적 및 할당 수를 포함하여 개발 중에 Android 애플리케이션을 검사하는 데 도움이 됩니다. Debug를 사용하여 런타임 및 네이티브 메모리 카운터, 특정 프로세스의 메모리 공간을 식별하는 데 도움이 될 수 있는 메모리 측정항목을 얻을 수도 있습니다.

ApplicationExitInfo

ApplicationExitInfo는 Android 11(API 수준 30) 이상에서 사용할 수 있으며 애플리케이션 종료 이유에 관한 정보를 제공합니다. 여기에는 ANR, 메모리 부족, 앱 비정상 종료, 과도한 CPU 사용, 사용자 중단, 시스템 중단 또는 런타임 권한 변경이 포함됩니다.

엄격 모드

StrictMode를 사용하면 앱을 개발하는 동안 기본 스레드에서 실수로 인한 I/O 작업을 찾을 수 있습니다. 애플리케이션 또는 활동 수준에서 StrictMode를 사용할 수 있습니다.

백그라운드 ANR 대화상자 사용 설정

Android는 기기의 개발자 옵션에서 모든 ANR 표시가 사용 설정된 경우에만 브로드캐스트 메시지를 처리하는 데 너무 오래 걸리는 앱에 관한 ANR 대화상자를 표시합니다. 따라서 앱에 성능 문제가 발생하고 있더라도 백그라운드 ANR 대화상자가 항상 사용자에게 표시되지는 않습니다.

Traceview

Traceview를 사용하여 사용 사례를 진행하는 동안 실행 중인 앱의 트레이스를 가져오고 기본 스레드를 사용 중인 위치를 식별할 수 있습니다. Traceview 사용 방법에 관한 자세한 내용은 Traceview 및 dmtracedump로 프로파일링을 참고하세요.

트레이스 파일 가져오기

Android에서는 ANR이 발생할 때 트레이스 정보를 저장합니다. 이전 OS 버전에는 기기에 하나의 /data/anr/traces.txt 파일이 있습니다. 최신 OS 버전에는 여러 개의 /data/anr/anr_* 파일이 있습니다. 루트로 Android 디버그 브리지(adb)를 사용하여 기기 또는 에뮬레이터에서 ANR 트레이스에 액세스할 수 있습니다.

adb root
adb shell ls /data/anr
adb pull /data/anr/<filename>

기기에서 버그 신고 개발자 옵션을 사용하거나 개발 머신에서 adb bugreport 명령어를 사용하여 실제 기기에서 버그 신고를 캡처할 수 있습니다. 자세한 내용은 버그 신고 캡처 및 읽기를 참고하세요.

문제 해결

문제를 식별한 후 이 섹션의 도움말을 사용하여 자주 발생하는 문제를 해결할 수 있습니다.

기본 스레드의 느린 코드

앱의 기본 스레드를 5초 이상 사용 중인 코드의 위치를 확인하세요. 앱에서 의심스러운 사용 사례를 찾아 ANR을 재현해 보세요.

예를 들어 그림 2는 기본 스레드를 5초 이상 사용 중인 Traceview 타임라인을 보여줍니다.

그림 2. 사용량이 많은 기본 스레드를 보여주는 Traceview 타임라인

그림 2. 사용량이 많은 기본 스레드를 보여주는 Traceview 타임라인

그림 2는 다음 코드 예에서와 같이 대부분의 문제 코드가 onClick(View) 핸들러에서 발생하는 것을 보여줍니다.

Kotlin

override fun onClick(v: View) {
    // This task runs on the main thread.
    BubbleSort.sort(data)
}

Java

@Override
public void onClick(View view) {
    // This task runs on the main thread.
    BubbleSort.sort(data);
}

이 경우 기본 스레드에서 실행되는 작업을 작업자 스레드로 이동해야 합니다. Android 프레임워크에는 작업을 작업자 스레드로 이동하는 데 도움이 되는 클래스가 포함되어 있습니다. 자세한 내용은 작업자 스레드를 참고하세요.

기본 스레드의 I/O

기본 스레드에서 I/O 작업을 실행하는 것은 기본 스레드의 작업이 느려지는 일반적인 원인이며 이로 인해 ANR이 발생할 수 있습니다. 이전 섹션에서 설명한 것처럼 모든 I/O 작업을 작업자 스레드로 이동하는 것이 좋습니다.

I/O 작업의 예로는 네트워크 및 저장소 작업이 있습니다. 자세한 내용은 네트워크 작업 실행데이터 저장을 참고하세요.

잠금 경합

일부 시나리오에서는 ANR을 유발하는 작업이 앱의 기본 스레드에서 직접 실행되지 않습니다. 작업자 스레드가 기본 스레드에서 작업을 완료하는 데 필요한 리소스의 잠금을 유지하는 경우 ANR이 발생할 수 있습니다.

예를 들어 그림 4에서는 대부분의 작업이 작업자 스레드에서 실행되는 Traceview 타임라인을 보여줍니다.

그림 4. 작업자 스레드에서 실행 중인 작업을 보여주는 Traceview 타임라인

그림 4. 작업자 스레드에서 실행 중인 작업을 보여주는 Traceview 타임라인

하지만 사용자에게 ANR이 계속 발생한다면 Android Device Monitor에서 기본 스레드의 상태를 확인해야 합니다. UI를 업데이트할 준비가 되어 있고 일반적으로 반응하는 기본 스레드는 대체로 RUNNABLE 상태입니다.

그러나 실행을 계속할 수 없는 기본 스레드는 BLOCKED 상태이며 이벤트에 응답할 수 없습니다. 그림 5에서와 같이 Android Device Monitor에 상태가 Monitor 또는 Wait로 표시됩니다.

그림 5. Monitor 상태의 기본 스레드

그림 5. Monitor 상태의 기본 스레드

다음 트레이스는 리소스 대기 중 차단된 기본 스레드를 보여줍니다.

...
AsyncTask #2" prio=5 tid=18 Runnable
  | group="main" sCount=0 dsCount=0 obj=0x12c333a0 self=0x94c87100
  | sysTid=25287 nice=10 cgrp=default sched=0/0 handle=0x94b80920
  | state=R schedstat=( 0 0 0 ) utm=757 stm=0 core=3 HZ=100
  | stack=0x94a7e000-0x94a80000 stackSize=1038KB
  | held mutexes= "mutator lock"(shared held)
  at com.android.developer.anrsample.BubbleSort.sort(BubbleSort.java:8)
  at com.android.developer.anrsample.MainActivity$LockTask.doInBackground(MainActivity.java:147)
  - locked <0x083105ee> (a java.lang.Boolean)
  at com.android.developer.anrsample.MainActivity$LockTask.doInBackground(MainActivity.java:135)
  at android.os.AsyncTask$2.call(AsyncTask.java:305)
  at java.util.concurrent.FutureTask.run(FutureTask.java:237)
  at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:243)
  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)
  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
  at java.lang.Thread.run(Thread.java:761)
...

트레이스를 검토하면 기본 스레드를 차단하는 코드를 찾는 데 도움이 될 수 있습니다. 다음 코드는 이전 트레이스에서 기본 스레드를 차단하는 잠금을 유지합니다.

Kotlin

override fun onClick(v: View) {
    // The worker thread holds a lock on lockedResource
    LockTask().execute(data)

    synchronized(lockedResource) {
        // The main thread requires lockedResource here
        // but it has to wait until LockTask finishes using it.
    }
}

class LockTask : AsyncTask<Array<Int>, Int, Long>() {
    override fun doInBackground(vararg params: Array<Int>): Long? =
            synchronized(lockedResource) {
                // This is a long-running operation, which makes
                // the lock last for a long time
                BubbleSort.sort(params[0])
            }
}

Java

@Override
public void onClick(View v) {
    // The worker thread holds a lock on lockedResource
   new LockTask().execute(data);

   synchronized (lockedResource) {
       // The main thread requires lockedResource here
       // but it has to wait until LockTask finishes using it.
   }
}

public class LockTask extends AsyncTask<Integer[], Integer, Long> {
   @Override
   protected Long doInBackground(Integer[]... params) {
       synchronized (lockedResource) {
           // This is a long-running operation, which makes
           // the lock last for a long time
           BubbleSort.sort(params[0]);
       }
   }
}

또 다른 예는 다음 코드에서와 같이 작업자 스레드의 결과를 대기 중인 기본 스레드입니다. 동시 실행을 처리하기 위한 고유한 메커니즘이 있는 Kotlin에서 wait()notify() 사용은 권장되는 패턴이 아닙니다. Kotlin을 사용할 때는 가능하면 Kotlin 전용 메커니즘을 사용해야 합니다.

Kotlin

fun onClick(v: View) {
    val lock = java.lang.Object()
    val waitTask = WaitTask(lock)
    synchronized(lock) {
        try {
            waitTask.execute(data)
            // Wait for this worker thread’s notification
            lock.wait()
        } catch (e: InterruptedException) {
        }
    }
}

internal class WaitTask(private val lock: java.lang.Object) : AsyncTask<Array<Int>, Int, Long>() {
    override fun doInBackground(vararg params: Array<Int>): Long? {
        synchronized(lock) {
            BubbleSort.sort(params[0])
            // Finished, notify the main thread
            lock.notify()
        }
    }
}

Java

public void onClick(View v) {
   WaitTask waitTask = new WaitTask();
   synchronized (waitTask) {
       try {
           waitTask.execute(data);
           // Wait for this worker thread’s notification
           waitTask.wait();
       } catch (InterruptedException e) {}
   }
}

class WaitTask extends AsyncTask<Integer[], Integer, Long> {
   @Override
   protected Long doInBackground(Integer[]... params) {
       synchronized (this) {
           BubbleSort.sort(params[0]);
           // Finished, notify the main thread
           notify();
       }
   }
}

리소스 풀(예: 데이터베이스 연결 풀) 또는 기타 상호 배제(뮤텍스) 메커니즘뿐만 아니라 Lock, Semaphore를 사용하는 스레드를 포함하여 기본 스레드를 차단할 수 있는 몇 가지 다른 상황이 있습니다.

앱에서 유지하는 리소스 잠금을 전체적으로 평가해야 하지만 ANR을 피하려면 기본 스레드에서 필요한 리소스에 유지되는 잠금을 확인해야 합니다.

잠금이 최소 시간 동안 유지되었는지 확인하거나, 더 나은 방법으로는 애초에 앱에 잠금 유지가 필요한지 평가합니다. 작업자 스레드 처리를 기반으로 UI를 업데이트하는 시기를 알아내기 위해 잠금을 사용한다면 작업자 스레드와 기본 스레드 간에 통신하는 데 onProgressUpdate()onPostExecute()와 같은 메커니즘을 사용합니다.

교착 상태

한 스레드에서 필요한 리소스를 다른 스레드가 보유하고 있고, 다른 스레드 역시 첫 번째 스레드가 보유 중인 리소스를 대기하고 있어 스레드가 대기 상태가 되는 경우 교착 상태가 발생합니다. 앱의 기본 스레드가 이 상황이면 ANR이 발생할 가능성이 큽니다.

교착 상태는 컴퓨터 공학에서 철저히 연구된 현상으로, 교착 상태 방지 알고리즘을 사용하여 교착 상태를 방지할 수 있습니다.

자세한 내용은 위키백과의 교착 상태교착 상태 방지 알고리즘을 참고하세요.

느린 broadcast receiver

앱은 broadcast receiver를 사용하여 비행기 모드 사용 설정 또는 중지나 연결 상태 변경과 같은 브로드캐스트 메시지에 응답할 수 있습니다. 앱이 브로드캐스트 메시지를 처리하는 데 너무 오래 걸리면 ANR이 발생합니다.

다음과 같은 경우에 ANR이 발생합니다.

  • broadcast receiver가 상당한 시간 내에 onReceive() 메서드 실행을 완료하지 못했습니다.
  • broadcast receiver가 goAsync()를 호출하지만 PendingResult 객체에서 finish()를 호출하지 못합니다.

앱은 BroadcastReceiveronReceive() 메서드에서 짧은 작업만 실행해야 합니다. 하지만 브로드캐스트 메시지로 인해 앱에 더 복잡한 처리가 필요하다면 작업을 IntentService로 이전해야 합니다.

Traceview와 같은 도구를 사용하면 broadcast receiver가 앱의 기본 스레드에서 장기 실행 작업을 실행하는지 확인할 수 있습니다. 예를 들어 그림 6에서는 약 100초 동안 기본 스레드에서 메시지를 처리하는 broadcast receiver의 타임라인을 보여줍니다.

그림 6. 기본 스레드의 &#39;BroadcastReceiver&#39; 작업을 보여주는 Traceview 타임라인

그림 6. 기본 스레드의 BroadcastReceiver 작업을 보여주는 Traceview 타임라인

다음 예에서와 같이 BroadcastReceiveronReceive() 메서드에서 장기 실행 작업을 실행하면 이 동작이 발생할 수 있습니다.

Kotlin

override fun onReceive(context: Context, intent: Intent) {
    // This is a long-running operation
    BubbleSort.sort(data)
}

Java

@Override
public void onReceive(Context context, Intent intent) {
    // This is a long-running operation
    BubbleSort.sort(data);
}

이 같은 상황에서는 장기 실행 작업을 IntentService로 이동하는 것이 좋습니다. 이 클래스는 작업자 스레드를 사용하여 작업을 실행하기 때문입니다. 다음 코드는 IntentService를 사용하여 장기 실행 작업을 처리하는 방법을 보여줍니다.

Kotlin

override fun onReceive(context: Context, intent: Intent) {
    Intent(context, MyIntentService::class.java).also { intentService ->
        // The task now runs on a worker thread.
        context.startService(intentService)
    }
}

class MyIntentService : IntentService("MyIntentService") {
    override fun onHandleIntent(intent: Intent?) {
        BubbleSort.sort(data)
    }
}

Java

@Override
public void onReceive(Context context, Intent intent) {
    // The task now runs on a worker thread.
    Intent intentService = new Intent(context, MyIntentService.class);
    context.startService(intentService);
}

public class MyIntentService extends IntentService {
   @Override
   protected void onHandleIntent(@Nullable Intent intent) {
       BubbleSort.sort(data);
   }
}

IntentService를 사용하면 장기 실행 작업이 기본 스레드 대신 작업자 스레드에서 실행됩니다. 그림 7은 Traceview 타임라인에서 작업자 스레드로 이전된 작업을 보여줍니다.

그림 7. 작업자 스레드에서 처리되는 브로드캐스트 메시지를 보여주는 Traceview 타임라인

그림 7. 작업자 스레드에서 처리되는 브로드캐스트 메시지를 보여주는 Traceview 타임라인

broadcast receiver는 goAsync()를 사용하여 시스템에 메시지를 처리하는 데 더 많은 시간이 필요하다는 신호를 보냅니다. 단, 개발자가 PendingResult 객체에서 finish()를 호출해야 합니다. 다음 예는 finish()를 호출하여 시스템에서 broadcast receiver를 재활용하고 ANR을 방지하도록 하는 방법을 보여줍니다.

Kotlin

val pendingResult = goAsync()

object : AsyncTask<Array<Int>, Int, Long>() {
    override fun doInBackground(vararg params: Array<Int>): Long? {
        // This is a long-running operation
        BubbleSort.sort(params[0])
        pendingResult.finish()
        return 0L
    }
}.execute(data)

Java

final PendingResult pendingResult = goAsync();
new AsyncTask<Integer[], Integer, Long>() {
   @Override
   protected Long doInBackground(Integer[]... params) {
       // This is a long-running operation
       BubbleSort.sort(params[0]);
       pendingResult.finish();
   }
}.execute(data);

그러나 브로드캐스트가 백그라운드에 있다면 느린 broadcast receiver에서 다른 스레드로 코드를 이동하고 goAsync()를 사용해도 ANR이 해결되지 않습니다. ANR 시간 제한이 계속 적용됩니다.

GameActivity

C 또는 C++로 작성된 게임과 앱의 우수사례를 보면 GameActivity 사용으로 인해 ANR이 줄었습니다. 기존 네이티브 액티비티를 GameActivity로 교체한다면 UI 스레드 차단을 줄이고 ANR이 발생하는 것을 어느 정도 막을 수 있습니다.

ANR에 관한 자세한 내용은 앱 응답성 유지를 참고하세요. 스레드에 관한 자세한 내용은 스레딩 성능을 참고하세요.

  • 참고: JavaScript가 사용 중지되어 있으면 링크 텍스트가 표시됩니다.
  • 불필요한 wakeup