英文原文:https://medium.com/firebase-developers/crashlytics-and-unity-part-1-ca0598862918
已经崩溃的将永远不会崩溃
我们在发布游戏之前尽可能多地测试游戏,但无论我们投入多少工作来确保我们的游戏没有错误,都不可避免地会漏掉一些东西。 当崩溃确实发生时,我们可以等待玩家将其变成在社交媒体上流行的 gif 动画,或者我们可以连接一个崩溃报告工具来汇总玩家遇到的问题。
我想向您介绍 Firebase 的崩溃聚合和报告解决方案。 为此,我将在 Unity 代码库中综合一些常见的崩溃原型,并向您展示 Crashlytics 如何报告它们。
设置
我假设您已经为您的应用安装并设置了 Firebase。 如果没有,请查看我们关于该主题的现有文档。 除了仅设置 Firebase 之外,您还应确保从用于设置项目的 Firebase SDK 添加 Crashlytics Unity SDK。
此外,请确保在您的 Firebase 控制台中启用了 Firebase。 为此,请转到侧边栏中的 Crashlytics 条目:
并确保明确启用集成:
为了测试 Crashlytics,我编写了一些旨在导致 C# 异常的示例 MonoBehaviour 脚本。 然后我将它们附加到一系列的按钮上,这样我就可以在设备上运行并生成一些不错的崩溃数据。 为清楚起见,我在下面内联了每个异常,但请随时将它们全部复制到您的测试项目中。
崩溃报告将自动批处理并发送到 Crashlytics 服务器,通常是在下次运行时。 因此,您应该重新启动您的游戏以查看以下代码片段中生成的任何异常(最显着的异常是如果您的游戏在 Android 上发生致命崩溃,所有待处理的崩溃日志都会在后台推送)。 Crashlytics 仪表板是实时的,这意味着我们通常可以在几秒钟内看到测试结果。
现在我们已经完成了设置,让我们写一些非常糟糕和不稳定的代码吧!
基本异常
我将从一个标准的 C# 异常开始,这种情况基本上发生在你最终做了一些你不应该做的事情的时候:
using System;
using UnityEngine;
public class TestBasicException : MonoBehaviour
{
public void TriggerException()
{
throw new Exception("Oh Bother");
}
}
当您展开 TestBasicException 崩溃时,您可以看到我们得到了一个非常方便的堆栈跟踪和有关它发生的设备的信息。 如果崩溃仅发生在某些用户身上,则设备信息特别有用。 您可能能够找出所有问题之间的共同操作系统功能(或缺乏),尤其是当您与 Firebase Analytics 进行比较时。
你可以在这里看到崩溃发生在 TestBasicException 上的方法 TriggerException 中。 这是您应该经常看到的那种异常。
空预制件——NullReferenceException
我没有确切的数字可以查看,但我在自己的游戏中看到的大多数崩溃似乎都是一些损坏的预制参考的变体。 通常这是由于合并或重构而发生的,但有时我只是忘记将一个东西拖到另一个东西中。 因此,如果您看到此脚本:
using UnityEngine;
public class TestPrefabReference : MonoBehaviour
{
[SerializeField] private GameObject _targetObject;
public void PrintTarget()
{
Debug.Log($"Target Position: {_targetObject.transform.position}");
}
}
以及 NullReferenceException 类型的 PrintTarget 中的堆栈跟踪:
您的第一反应可能应该是检查此脚本的用法并确保预制引用都已连接。
协程异常
有经验的 Unity 开发人员的名片往往是协程的使用。 特别是在引擎历史的早期,这是异步执行某些逻辑而不会导致线程错误的最可靠方法。 所以,让我们构建一个简单的协程,它会抛出:
using System;
using System.Collections;
using UnityEngine;
public class TestBasicExceptionInCoroutine : MonoBehaviour
{
public void TriggerException()
{
StartCoroutine(TriggerCoroutine());
}
private IEnumerator TriggerCoroutine()
{
yield return null;
throw new Exception("Oh Co-Bother...");
}
}
导致这个看起来很奇怪的调用堆栈:
这里要注意的最重要的一点是 InvokeMoveNext 表示这可能是一个协程,它发生在的类是 TestBasicExceptionInCoroutine,+< TriggerCoroutine > 基本上意味着你在这个类上名为 TriggerCoroutine 的函数中。
如果您像我一样对这个答案不满意,我们需要更深入地了解 Unity 协程的工作原理。 通常,要迭代集合,您可以让集合返回一个 IEnumerator。 然后,您可以通过以下方式遍历集合:
IEnumerator iter = myList.GetEnumerator();
while(iter.MoveNext()) {
DoSomething(iter.Current);
}
或者使用速记:
for(var i in myList) {
DoSomething(i);
}
在幕后,当您有一个返回 IEnumerator 或 IEnumerator< T > 的函数并且有任何 yield 返回指令时,它会自动生成一个符合 IEnumerator 接口的类。 当您调用 StartCoroutine() 时,传递给它的 IEnumerator 会缓存在某处的银行中。 然后以固定间隔(通常在 Update 和 LateUpdate 之间)对所有以这种方式启动的协程调用 MoveNext()。
在这种情况下,我们看到隐式生成的 MoveNext() 已在某些 Unity 函数 UnityEngine.SetupCoroutine.InvokeMoveNext 中被调用。 我们在此生成的代码中遇到了异常。 确切生成的内部类可能取决于您使用的编译器,在这种情况下它是 < TriggerCoroutine >d_1 类,对应于我们编写的 IEnumerator TriggerCoroutine() 函数。 这不是最漂亮的调用堆栈,但它仍然将我们带到问题发生的确切位置。
线程异常
我要强调的最后一种情况是在后台线程中抛出异常。 Unity 引擎中的许多类都不是线程安全的,并且 FirebaseApp.CheckDependenciesAsync() 等函数可能会在后台执行您的延续逻辑。 如果您不知道要查找什么,调试可能需要一些时间。 我们看一下后台抛出的异常:
using System;
using System.Threading;
using UnityEngine;
public class TestThreadException : MonoBehaviour
{
public void TriggerException()
{
var thread = new Thread(() => throw new Exception("Oh Noes, I crashed in the background!"));
thread.Start();
}
}
当您在 Crashlytics 仪表板中检查异常时:
它现在包含许多对 System.Threading 的引用,而不是 thread.Start() 的堆栈跟踪。 这表明我们可能在后台运行。
为了使事情进一步复杂化,我选择使用 lambda 函数(一种非常常见的模式)来表示我的背景工作。 因此,除了令人困惑的调用堆栈之外,我还在 +<>c.< TriggerException >b__0_0 中看到了生成代码的指纹。 虽然我不知道这个线程从哪里开始,但我至少可以看到它发生在类 TestThreadException 中,并且我的 lambda 表达式周围的方法是 TriggerException。
好消息是,如果此代码由于线程而显式崩溃,并且您正在使用 .ContinueWith(),那么在您的 lambda 表达式之后附加 TaskScheduler.FromCurrentSynchronizationContext() 以强制您继续到主线程(如果您 在主线程上调用 .ContinueWith())。 此外,较新版本的 Firebase SDK 还包含一个扩展 .ContinueWithOnMainThread() ,您可以使用它来代替 .ContinueWith() 以强制将您的继续执行到主线程。
接下来是什么?
在这一点上,您希望对默认情况下 Crashlytics 可以为您的游戏带来什么有了一个很好的了解。 您只需将插件添加到您的 iOS 或 Android 游戏中,您就会立即开始从您的真实用户群中获取崩溃数据。 在我的下一篇文章中,我将讨论如何使用自定义键增强您从 Crashlytics 获得的信息以及交叉引用您手头可能拥有的其他数据集。