일반적인 Unity 게임 ANR

Unity ANR은 다양한 이유로 발생합니다. 가장 일반적인 ANR은 Android 및 Unity 구성요소의 오용과 잘못된 커뮤니케이션으로 인해 발생합니다.

WebView

WebView은 웹페이지를 표시하는 Android 클래스입니다. 서드 파티 SDK (예: 광고)는 WebView를 사용하여 UnityPlayerActivity 이외의 활동에 동적 웹 콘텐츠를 표시합니다. ANR은 서드 파티 SDK가 WebView를 잘못 사용하는 경우 발생합니다.

스택 트레이스

스택 트레이스는 ANR의 원인을 파악하는 첫 번째 수단입니다.

/data/app/~~p-0ksfCD6bF6Sdq6kpVePg==/com.google.android.webview-5YQZOqKbbqp-uoLY6WYnTw==/base.apk!libmonochrome.so
  at J.N.Mhc_M_H$ (Native method)
  at org.chromium.components.viz.service.frame_sinks.ExternalBeginFrameSourceAndroid.doFrame (chromium-TrichromeWebViewGoogle.aab-stable-579013831:60)
  at android.view.Choreographer$CallbackRecord.run (Choreographer.java:1054)
  at android.view.Choreographer.doCallbacks (Choreographer.java:878)
  at android.view.Choreographer.doFrame (Choreographer.java:807)
  at android.view.Choreographer$FrameDisplayEventReceiver.run (Choreographer.java:1041)
  at android.os.Handler.handleCallback (Handler.java:938)
  at android.os.Handler.dispatchMessage (Handler.java:99)
  at android.os.Looper.loop (Looper.java:223)
  at android.app.ActivityThread.main (ActivityThread.java:7721)
  at java.lang.reflect.Method.invoke (Native method)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:592)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:952)

그림 1.futex 대기로 인해 발생한 ANR 스택 트레이스

원인

현재까지 이 문제의 근본 원인은 명확하지 않습니다. 가능한 원인은 다음과 같습니다.

  • 잘못된 광고 구현
  • 사용자가 앱을 자동으로 업데이트하지 않도록 선택했을 수 있으므로 WebView의 버전이 오래되었습니다.
  • 시스템 리소스 (CPU, GPU 등)의 사용량이 많아 프로파일링이 많이 필요할 수 있습니다.
  • 셰이더 컴파일이 비정상 종료됩니다. 이는 콘텐츠에 호환되지 않는 셰이더가 있거나 사용자에게 이전 WebView 버전이 설치되어 있음을 나타낼 수 있습니다.

해결 방법

  • WebView로 인해 기본 스레드가 차단되는 콘텐츠 유형을 좁히려면 웹페이지가 로드되거나 표시되거나 닫힐 때마다 게임에 로그를 추가하세요.
    • Backtrace 또는 Crashlytics 보고 서비스를 사용할 수 있습니다.
    • 그런 다음 데이터를 분석하여 문제를 찾은 후 문제가 있는 광고 제공업체를 사용 중지해 보세요.
    • 문제가 메모리와 관련이 없는지 확인하기 위해 메모리 로그를 포함합니다.
  • 사용자에게 Google Play에서 WebView를 업데이트하라고 알립니다. Android 5.0(API 수준 21) 이상에서는 WebView가 APK로 이동했습니다. 따라서 Android 플랫폼과 별도로 업데이트할 수 있습니다. 기기에서 사용 중인 WebView 버전을 확인하려면 설정 > 앱 > Android 시스템 WebView로 이동하여 페이지 하단의 버전을 확인합니다.
WebView 버전을 보여주는 앱 정보 화면
그림 1. WebView 버전을 확인합니다.

Unity 일시중지

UnityPlayerActivityonPause() 호출을 수신하면 다음 작업 체인이 시작됩니다.

  1. UnityPlayerActivity는 활동이 일시중지되었음을 Unity 런타임 엔진에 알립니다.
  2. Unity는 OnApplicationPause 이벤트를 구현하는 모든 MonoBehaviour을 호출합니다.
  3. Unity는 사운드 재생, 렌더링, 게임 루프, 애니메이션과 같은 구성요소와 모듈을 중지합니다.
  4. Unity Android Player (UAP)과 엔진이 모두 동기화되도록 UAP는 엔진이 중지될 때까지 4초 동안 기다립니다.
  5. 작업이 5초 이상 걸리면 시스템에서 ANR을 트리거합니다.

스택 트레이스

"main" tid=1 Timed Waiting
jdk.internal.misc.Unsafe.park (Native method)
java.util.concurrent.locks.LockSupport.parkNanos (LockSupport.java:234)
java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedNanos (AbstractQueuedSynchronizer.java:1079)
java.util.concurrent.locks.AbstractQueuedSynchronizer.tryAcquireSharedNanos (AbstractQueuedSynchronizer.java:1369)
java.util.concurrent.Semaphore.tryAcquire (Semaphore.java:415)
com.unity3d.player.UnityPlayer.pauseUnity (UnityPlayer.java:833)
com.unity3d.player.UnityPlayer.pause (UnityPlayer.java:796)
com.unity3d.player.UnityPlayerActivity.onPause (UnityPlayerActivity.java:117)
android.app.Activity.performPause (Activity.java:8517)
android.app.Instrumentation.callActivityOnPause (Instrumentation.java:1618)
android.app.ActivityThread.performPauseActivityIfNeeded (ActivityThread.java:5061)
android.app.ActivityThread.performPauseActivity (ActivityThread.java:5022)
android.app.ActivityThread.handlePauseActivity (ActivityThread.java:4974)
android.app.servertransaction.PauseActivityItem.execute (PauseActivityItem.java:48)
android.app.servertransaction.ActivityTransactionItem.execute (ActivityTransactionItem.java:45)
android.app.servertransaction.TransactionExecutor.executeLifecycleState (TransactionExecutor.java:179)
android.app.servertransaction.TransactionExecutor.execute (TransactionExecutor.java:97)
android.app.ActivityThread$H.handleMessage (ActivityThread.java:2303)
android.os.Handler.dispatchMessage (Handler.java:106)
android.os.Looper.loopOnce (Looper.java:201)
android.os.Looper.loop (Looper.java:288)
android.app.ActivityThread.main (ActivityThread.java:7884)
java.lang.reflect.Method.invoke (Native method)
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:548)
com.android.internal.os.ZygoteInit.main (ZygoteInit.java:936)

그림 3. 해제되지 않는 세마포어로 인해 ANR이 발생합니다.

해결 방법

일시중지 또는 재개 이벤트 중에 C# 게임 코드가 실행을 완료하는 데 너무 오래 걸리지 않도록 해야 합니다.

  • 게임을 프로파일링하고 OnApplicationPause가 비용이 많이 드는 작업인지 확인합니다. Stopwatch를 사용할 수 있습니다.
  • I/O 작업 또는 동기 네트워크 요청을 피하세요.
  • Task를 사용하여 작업을 다른 Thread로 이동합니다. Unity 2023.1은 C# asyncawait 키워드를 사용하여 간소화된 비동기 프로그래밍 모델을 지원합니다.

UnitySendMessage 차단됨

Java Unity 플러그인과 SDK는 JNI를 사용하여 C# 게임 레이어로 데이터를 전송합니다. 하지만 이 통신은 뮤텍스와 같은 네이티브 동기화 루틴으로 인해 기본 스레드를 차단하여 잠금 경합으로 인한 ANR을 발생시킬 수 있습니다.

스택 트레이스

그림 4의 ANR은 Java 플러그인에서 호출한 C# 코드의 긴 작업으로 인해 발생했습니다. Unity 엔진은 비우선순위 상속 뮤텍스를 사용하여 올바른 실행을 보장합니다.

libc.so NonPI::MutexLockWithTimeout(pthread_mutex_internal_t*, bool, timespec const*) + 604
com.unity3d.player.UnityPlayer.nativeUnitySendMessage (Native method)
com.unity3d.player.UnityPlayer.UnitySendMessage (UnityPlayer.java:665)

그림 4. 잠금 경합으로 인해 ANR이 발생했습니다.

원인

문제는 애플리케이션이 재개될 때 여러 메시지가 디스패치된다는 것입니다. 게임이 백그라운드에 있는 동안에는 메시지를 보낼 수 없으므로 메시지가 대기열에 추가됩니다. 앱이 재개되면 메시지가 모두 동시에 디스패치됩니다.

일시중지 기간에는 일반적으로 게임의 정보를 서버에 저장합니다. 예를 들어 게임이 재개될 때 플레이어가 동일한 위치로 돌아갈 수 있도록 게임에서 플레이어의 위치를 기록합니다.

이 워크로드는 자체 워크로드를 만드는 다른 서드 파티 코드와 결합되어 기기의 리소스, 특히 기본 스레드를 과부하할 수 있습니다. 기본 스레드는 앱의 사용자 인터페이스를 실행하며 ANR이 발생하는 주요 위치인 경우가 많습니다. 따라서 기본 스레드에 추가된 작업량은 ANR 발생 가능성을 높입니다.

해결 방법

애플리케이션 일시중지 중에 모든 코드 작업이 필요한지 확인하거나 로컬 기기 메모리에 사용자 상태를 저장해 보세요. 일시중지 기간 외에도 이러한 작업을 완료할 수 있는지 확인하세요.

몇 가지 접근 방식:

  • 메시지를 처리하는 C# 작업을 기본 스레드가 아닌 스레드로 이동합니다.
    • 코드가 Unity의 기본 스레드 컨텍스트에 종속되지 않는 경우 메시지 대신 Task를 사용하여 통신하세요.
  • 게임이 일시중지된 경우 플러그인에서 여러 메시지를 보내지 마세요.
    • 게임이 백그라운드에 있는 동안에는 엔진에서 메시지를 보낼 수 없습니다.
    • 게임 기능에 영향을 미치지 않는 경우에만 마지막 데이터 상태를 게임에 전송하세요.

설치 리퍼러

Play Install Referrer는 사용자가 광고를 클릭할 때마다 Play 스토어로 전송되는 고유한 문자열입니다. Android 전용 광고 추적 식별자입니다. 설치되면 앱은 설치 리퍼러를 기여 분석 파트너에게 전송하고, 기여 분석 파트너는 소스를 설치와 일치시켜 전환을 기여 분석합니다.

스택 트레이스

그림 5는 Facebook SDK를 사용하여 설치 기여도를 가져오는 게임의 ANR 스택 트레이스를 보여줍니다.

그림 5. 바인더 호출이 포함된 Android vitals 보고서

원인

느린 바인더 호출로 인해 ANR이 발생했습니다. 하지만 SDK 소스 코드에 액세스하지 않고는 근본 원인을 확인할 수 없습니다.

해결 방법

이러한 유형의 문제를 해결하려면 SDK 개발자와 소통하거나, 잠재적인 해결책을 찾기 위해 온라인에서 많은 검색을 하거나, 최신 버전의 SDK가 다른 사용자의 ANR을 해결하는지 확인하거나, 소규모 출시 전략을 실험해야 합니다.

Google에서는 Google Play 앱의 사용 데이터와 코드 감지를 통해 수집된 정보를 조합하여 앱에서 SDK를 채택, 유지 또는 삭제할지 결정하는 데 도움이 되도록 설계된 속성 및 신호를 제공하는 SDK 색인 페이지를 제공합니다.

추가 리소스

ANR에 관해 자세히 알아보려면 다음 리소스를 참고하세요.