Google은 흑인 공동체를 위한 인종 간 평등을 진전시키기 위해 노력하고 있습니다. Google에서 어떤 노력을 하고 있는지 확인하세요.

ANR

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

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

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

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

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

  • 활동이 포그라운드에 있는 동안 앱이 입력 이벤트 또는 BroadcastReceiver(예: 키 누름 또는 화면 터치 이벤트)에 5초 이내에 응답하지 않았습니다.
  • 포그라운드에 활동이 없을 때 BroadcastReceiver가 상당한 시간 내에 실행을 완료하지 못했습니다.

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

문제 감지 및 진단

Android에서는 여러 가지 방법으로 앱 문제를 알리고 문제 진단을 도와 줍니다. 앱을 이미 게시한 경우 Android vitals에서 개발자에게 문제가 발생했다고 알릴 수 있으며 문제를 찾는 데 도움이 되는 진단 도구가 있습니다.

Android vitals

Android vitals를 사용하면 앱에서 ANR이 과도하게 발생하는 경우 Play Console을 통해 알림을 보냄으로써 앱의 성능을 개선할 수 있습니다. Android vitals는 앱이 다음과 같을 때 ANR이 과도하다고 간주합니다.

  • 일일 세션의 0.47% 이상에서 한 번 이상의 ANR이 나타납니다.
  • 일일 세션의 0.24% 이상에서 두 번 이상의 ANR이 나타납니다.

일일 세션이란 앱이 사용된 1일을 의미합니다.

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

ANR 진단

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

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

다음 기법을 통해 ANR을 유발하는 원인을 파악할 수 있습니다.

엄격 모드

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)
    }
    

자바

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

이 경우 기본 스레드에서 실행되는 작업을 작업자 스레드로 이동해야 합니다. Android 프레임워크에는 작업을 작업자 스레드로 이동하는 데 도움이 되는 클래스가 포함되어 있습니다. 자세한 내용은 스레딩을 위한 도우미 클래스를 참조하세요. 다음 코드는 AsyncTask 도우미 클래스를 사용하여 작업자 스레드에서 작업을 처리하는 방법을 보여줍니다.

Kotlin

    override fun onClick(v: View) {
        // The long-running operation is run on a worker thread
        object : AsyncTask<Array<Int>, Int, Long>() {
            override fun doInBackground(vararg params: Array<Int>): Long? =
                BubbleSort.sort(params[0])

        }.execute(data)
    }
    

자바

    @Override
    public void onClick(View view) {
       // The long-running operation is run on a worker thread
       new AsyncTask<Integer[], Integer, Long>() {
           @Override
           protected Long doInBackground(Integer[]... params) {
               BubbleSort.sort(params[0]);
           }
       }.execute(data);
    }
    

그림 3에서와 같이 Traceview에 대부분의 코드가 작업자 스레드에서 실행된다고 표시됩니다. 기본 스레드가 사용자 이벤트에 응답할 수 있습니다.

그림 3. 작업자 스레드에서 처리되는 작업을 보여주는 Traceview 타임라인

그림 3. 작업자 스레드에서 처리되는 작업을 보여주는 Traceview 타임라인

기본 스레드의 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])
                }
    }
    

자바

    @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()
            }
        }
    }
    

자바

    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. 기본 스레드의 BroadcastReceiver 작업을 보여주는 Traceview 타임라인

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

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

Kotlin

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

자바

    @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)
        }
    }
    

자바

    @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)
    

자바

    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 시간 제한이 계속 적용됩니다.

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