앱 응답성 유지

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

작성한 코드가 전 세계 모든 성능 테스트에서 우승할 정도의 성능을 갖춰도 여전히 중요한 순간에 느리게 느껴지고 작동이 멈추거나 입력을 처리하는 데 너무 오래 걸릴 수 있습니다. 앱 응답에서 나타날 수 있는 최악의 경우는 '애플리케이션 응답 없음'(ANR) 대화상자입니다.

Android에서 시스템은 일정 기간 충분한 속도로 응답하지 않는 애플리케이션이 있으면 그림 1의 대화상자와 같이 앱의 응답이 멈추었다고 알리는 대화상자를 표시하여 보호 기능을 적용합니다. 이 시점에서 앱은 상당한 시간 동안 응답하지 않았으므로 시스템에서 사용자에게 앱을 종료하는 옵션을 제공합니다. 애플리케이션에 응답성을 설계하여 시스템이 사용자에게 ANR 대화상자를 표시하지 않도록 하는 것이 중요합니다.

이 문서에서는 애플리케이션의 응답 여부를 Android 시스템이 판단하는 방법에 관해 설명하고 애플리케이션의 응답을 유지하기 위한 가이드라인을 제공합니다.

ANR을 유발하는 요인은 무엇입니까?

보통 애플리케이션이 사용자 입력에 응답할 수 없으면 시스템에서 ANR을 표시합니다. 예를 들어 애플리케이션이 UI 스레드에서 일부 I/O 작업(종종 네트워크 액세스)을 차단하면, 시스템은 들어오는 사용자 입력 이벤트를 처리할 수 없습니다. 또는 앱이 정교한 메모리 내 구조를 구축하거나 UI 스레드에서 게임의 다음 동작을 계산하는 데 너무 많은 시간을 소비하는 것일 수도 있습니다. 항상 이러한 계산이 효율적인지 확인하는 것이 중요하지만, 가장 효율적인 코드조차도 실행하는 데 시간이 걸립니다.

앱이 잠재적으로 오랜 시간 작업을 수행하는 상황에서는 UI 스레드에서 작업을 수행하는 대신 작업자 스레드를 만들고 거기에서 대부분의 작업을 수행해야 합니다. 이렇게 하면 사용자 인터페이스 이벤트 루프를 구동하는 UI 스레드가 계속 실행되므로 시스템에서는 코드가 멈추었다는 결론을 내리지 못하게 됩니다. 이러한 스레딩은 대개 클래스 수준에서 실행되기 때문에 응답성을 클래스 문제로 생각할 수 있습니다. 이것을 메서드 수준 문제인 기본 코드 성능과 비교해 보세요.

Android에서는 Activity Manager 및 Window Manager 시스템 서비스에서 애플리케이션 응답성을 모니터링합니다. Android는 다음 조건 중 하나를 감지하면 애플리케이션에 대해 ANR 대화상자를 표시합니다.

  • 입력 이벤트(예: 키 누름 또는 화면 터치 이벤트)에 5초 내에 응답하지 않음
  • BroadcastReceiver가 10초 내에 실행을 완료하지 못함

ANR을 피하는 방법

Android 애플리케이션은 일반적으로 단일 스레드(기본적으로 'UI 스레드' 또는 '주 스레드')에서 전적으로 실행됩니다. 즉, 애플리케이션이 UI 스레드에서 완료하는 데 오랜 시간이 걸리는 작업을 수행하는 경우 ANR 대화상자가 트리거될 수 있습니다. 이는 애플리케이션이 입력 이벤트 또는 인텐트 브로드캐스트를 처리할 기회를 제공하지 않기 때문입니다.

따라서 UI 스레드에서 실행되는 메서드는 가능한 한 작업을 적게 실행해야 합니다. 특히 활동은 가능한 한 적게 실행되어 onCreate()onResume()과 같은 주요 수명 주기 메서드에서 설정해야 합니다. 네트워크나 데이터베이스 작업과 같이 장기 실행 가능성이 있는 작업이나 비트맵 크기 조정과 같이 계산이 많이 필요한 작업은 작업자 스레드에서(또는 데이터베이스 작업의 경우 비동기식 요청을 통해) 실행해야 합니다.

시간이 오래 걸리는 작업을 위한 작업자 스레드를 만드는 가장 효과적인 방법은 AsyncTask 클래스를 사용하는 것입니다. AsyncTask를 확장하고 doInBackground() 메서드를 구현하여 작업을 실행하기만 하면 됩니다. 사용자에게 진행률 변경사항을 게시하려면 onProgressUpdate() 콜백 메서드를 호출하는 publishProgress()를 호출하면 됩니다. UI 스레드에서 실행되는 onProgressUpdate()를 구현한 후에는 사용자에게 알릴 수 있습니다. 예:

Kotlin

    private class DownloadFilesTask : AsyncTask<URL, Int, Long>() {

        // Do the long-running work in here
        override fun doInBackground(vararg urls: URL): Long? {
            val count: Float = urls.size.toFloat()
            var totalSize: Long = 0
            urls.forEachIndexed { index, url ->
                totalSize += Downloader.downloadFile(url)
                publishProgress((index / count * 100).toInt())
                // Escape early if cancel() is called
                if (isCancelled) return totalSize
            }
            return totalSize
        }

        // This is called each time you call publishProgress()
        override fun onProgressUpdate(vararg progress: Int?) {
            setProgressPercent(progress.firstOrNull() ?: 0)
        }

        // This is called when doInBackground() is finished
        override fun onPostExecute(result: Long?) {
            showNotification("Downloaded $result bytes")
        }
    }
    

자바

    private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
        // Do the long-running work in here
        protected Long doInBackground(URL... urls) {
            int count = urls.length;
            long totalSize = 0;
            for (int i = 0; i < count; i++) {
                totalSize += Downloader.downloadFile(urls[i]);
                publishProgress((int) ((i / (float) count) * 100));
                // Escape early if cancel() is called
                if (isCancelled()) break;
            }
            return totalSize;
        }

        // This is called each time you call publishProgress()
        protected void onProgressUpdate(Integer... progress) {
            setProgressPercent(progress[0]);
        }

        // This is called when doInBackground() is finished
        protected void onPostExecute(Long result) {
            showNotification("Downloaded " + result + " bytes");
        }
    }
    

이 작업자 스레드를 실행하려면 인스턴스를 만들고 execute()를 호출하면 됩니다.

Kotlin

    DownloadFilesTask().execute(url1, url2, url3)
    

자바

    new DownloadFilesTask().execute(url1, url2, url3);
    

AsyncTask보다 더 복잡하기는 하지만 자체 Thread 또는 HandlerThread 클래스를 대신 만드는 것이 좋을 수 있습니다. 만든다면 Process.setThreadPriority()를 호출하고 THREAD_PRIORITY_BACKGROUND를 전달하여 스레드 우선순위를 '백그라운드' 우선순위로 설정해야 합니다. 이렇게 스레드를 낮은 우선순위로 설정하지 않으면 스레드는 기본적으로 UI 스레드와 동일한 우선순위로 작동하므로 여전히 앱 속도를 늦출 수 있습니다.

Thread 또는 HandlerThread를 구현하는 경우 작업자 스레드가 완료되기를 기다리는 동안 UI 스레드가 차단되지 않아야 합니다. Thread.wait() 또는 Thread.sleep()을 호출하지 마세요. 작업자 스레드가 완료되기를 기다리는 동안 차단하는 대신 기본 스레드는 다른 스레드에 Handler를 제공하여 완료 시 다시 게시해야 합니다. 이 방식으로 애플리케이션을 설계하면 앱의 UI 스레드가 입력에 응답할 수 있으므로, 5초 입력 이벤트 시간 초과로 인해 ANR 대화상자가 표시되는 상황을 피할 수 있습니다.

BroadcastReceiver 실행 시간을 구체적으로 제한하면 broadcast receiver가 해야 할 일, 즉 설정 저장이나 Notification 등록과 같이 백그라운드에서 실행되는 별개의 작은 작업들이 강조됩니다. 따라서 UI 스레드에서 호출되는 다른 메서드와 마찬가지로 애플리케이션은 broadcast receiver에서 장기 실행 가능성이 있는 작업이나 계산을 피해야 합니다. 그러나 인텐트 브로드캐스트에 응답하여 장기 실행 가능성이 있는 작업을 처리해야 하는 경우 애플리케이션은 작업자 스레드를 통해 집중적인 작업을 실행하는 대신 IntentService를 시작해야 합니다.

BroadcastReceiver 객체의 또 다른 일반적인 문제는 객체가 너무 자주 실행될 때 발생합니다. 백그라운드에서 자주 실행하면 다른 앱에서 사용할 수 있는 메모리가 줄어듭니다. BroadcastReceiver 객체를 효율적으로 사용 설정 및 사용 중지하는 방법에 관한 자세한 내용은 필요 시 Broadcast Receiver 조작을 참조하세요.

도움말: StrictMode를 사용하면 실수로 기본 스레드에서 실행하고 있을 수 있는 장기 실행 가능성이 있는 작업(예: 네트워크 또는 데이터베이스 작업)을 찾는 데 도움이 됩니다.

응답성 강화

일반적으로 100~200ms는 사용자가 애플리케이션의 속도 저하를 감지하는 임계값입니다. 따라서 ANR을 피하고 애플리케이션이 적절한 속도로 사용자에게 응답하도록 하기 위해 추가적으로 할 수 있는 작업이 있습니다.

  • 애플리케이션이 사용자 입력에 응답하여 백그라운드에서 작업을 실행하는 경우 진행 상황을 보여줍니다(예: UI의 ProgressBar).
  • 특히 게임의 경우 작업자 스레드에서 이동을 계산합니다.
  • 애플리케이션에 시간이 많이 소요되는 초기 설정 단계가 있는 경우, 스플래시 화면을 표시하거나 기본 보기를 가능한 한 빨리 렌더링하고, 로드가 진행 중임을 나타나며 비동기로 정보를 채웁니다. 어떤 경우든, 애플리케이션이 멈추지 않았음을 사용자가 인지할 수 있도록 진행 상황을 표시해야 합니다.
  • SystraceTraceview와 같은 성능 도구를 사용하여 앱 응답성의 병목 현상을 확인합니다.