Unity 游戏常见的 ANR

发生 Unity ANR 的原因有很多。最常见的 ANR 问题是由 Android 和 Unity 组件滥用及其通信不畅导致的。

WebView

WebView 是一个用于显示网页的 Android 类。第三方 SDK(例如广告)使用 WebViewUnityPlayerActivity 之外的 activity 中显示动态 Web 内容。当第三方 SDK 滥用 WebView 时,就会发生 ANR。

堆栈轨迹

堆栈轨迹是您了解 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 阻塞主线程的内容类型,请在加载、显示或关闭网页时向游戏添加日志。
    • 您可以使用 BacktraceCrashlytics 报告服务。
    • 然后,在分析数据并找出问题后,尝试停用违规的广告提供商。
    • 添加内存日志,确保问题与内存无关。
  • 提醒用户在 Google Play 中更新 WebView。从 Android 5.0(API 级别 21)及更高版本开始,WebView 已转为 APK。因此,它可以独立于 Android 平台进行更新。如需了解设备上正在使用的 WebView 版本,请依次前往设置 > 应用 > Android System WebView,然后查看页面底部的版本。
显示 WebView 版本的应用信息界面。
图 1.查看 WebView 版本。

Unity 暂停

UnityPlayerActivity 收到 onPause() 调用时,便会启动以下操作链:

  1. UnityPlayerActivity 会通知 Unity 运行时引擎 activity 已暂停。
  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. 包含 Binder 调用的 Android Vitals 报告。

原因

ANR 是由 binder 调用速度缓慢引起的。不过,如果不访问 SDK 源代码,则无法确定根本原因。

解决方案

若要解决这类问题,您需要与 SDK 开发者沟通,或大量在线搜索潜在解决方案,检查较新版本的 SDK 能否为其他人解决 ANR,甚至尝试采用小规模的发布策略。

Google 提供了一个 SDK 索引页面,该页面将 Google Play 应用中的使用情况数据与通过代码检测收集到的信息相结合,提供属性和信号,旨在帮助您决定在应用中采用、保留还是移除某个 SDK。

其他资源

如需详细了解 ANR,请参阅以下资源: