Unity3D的全局异常处理策略是什么?

the*_*dox 21 c# unity-game-engine

我正在研究做一些Unity3D脚本的东西,我想建立全局异常处理系统.这不是为了在游戏的发布版本中运行,目的是捕获用户脚本和编辑器脚本中的异常,并确保将它们转发到数据库进行分析(以及向相关开发人员发送电子邮件,以便他们可以修复他们的shizzle).

在一个vanilla C#应用程序中,我有一个围绕Main方法的try-catch.在WPF中,我将挂钩一个或多个未处理的异常事件.在Unity ......?

到目前为止,我能想出的最好的是这样的:

using UnityEngine;
using System.Collections;

public abstract class BehaviourBase : MonoBehaviour {

    // Use this for initialization
    void Start () {

    }

    // Update is called once per frame
    void Update () {
        try
        {
            performUpdate();
            print("hello");
        }
        catch (System.Exception e)
        {
            print(e.ToString());
        }

    }

    public abstract void performUpdate();

}
Run Code Online (Sandbox Code Playgroud)

在其他脚本中,我派生出BehaviourBase而不是MonoBehavior,并实现performUpdate()而不是Update().我没有为Editor clases实现并行版本,但我认为我必须在那里做同样的事情.

但是,我不喜欢这种策略,因为我必须将它反向移植到我们从社区中获取的任何脚本(我将不得不在团队中执行它).编辑器脚本也没有与MonoBehavior相当的单一入口点,因此我假设我必须实现向导,编辑器等的异常安全版本.

我已经看到了使用Application.RegisterLogCallback捕获日志消息(而不是异常)的建议 ,但这让我感到不舒服,因为我需要解析调试日志字符串而不是访问实际的异常和堆栈跟踪.

那么......做对的是什么?

Bry*_*end 12

我在这里找到了RegisterLogCallback的工作实现:http://answers.unity3d.com/questions/47659/callback-for-unhandled-exceptions.html

在我自己的实现中,我使用它来调用我自己的MessageBox.Show而不是写入日志文件.我只是从我的每个场景调用SetupExceptionHandling.

    static bool isExceptionHandlingSetup;
    public static void SetupExceptionHandling()
    {
        if (!isExceptionHandlingSetup)
        {
            isExceptionHandlingSetup = true;
            Application.RegisterLogCallback(HandleException);
        }
    }

    static void HandleException(string condition, string stackTrace, LogType type)
    {
        if (type == LogType.Exception)
        {
            MessageBox.Show(condition + "\n" + stackTrace);
        }
    }
Run Code Online (Sandbox Code Playgroud)

我现在也有错误处理程序通过这个例程给我发电子邮件,所以我总是知道我的应用程序何时崩溃并获得尽可能详细的信息.

        internal static void ReportCrash(string message, string stack)
    {
        //Debug.Log("Report Crash");
        var errorMessage = new StringBuilder();

        errorMessage.AppendLine("FreeCell Quest " + Application.platform);

        errorMessage.AppendLine();
        errorMessage.AppendLine(message);
        errorMessage.AppendLine(stack);

        //if (exception.InnerException != null) {
        //    errorMessage.Append("\n\n ***INNER EXCEPTION*** \n");
        //    errorMessage.Append(exception.InnerException.ToString());
        //}

        errorMessage.AppendFormat
        (
            "{0} {1} {2} {3}\n{4}, {5}, {6}, {7}x {8}\n{9}x{10} {11}dpi FullScreen {12}, {13}, {14} vmem: {15} Fill: {16} Max Texture: {17}\n\nScene {18}, Unity Version {19}, Ads Disabled {18}",
            SystemInfo.deviceModel,
            SystemInfo.deviceName,
            SystemInfo.deviceType,
            SystemInfo.deviceUniqueIdentifier,

            SystemInfo.operatingSystem,
            Localization.language,
            SystemInfo.systemMemorySize,
            SystemInfo.processorCount,
            SystemInfo.processorType,

            Screen.currentResolution.width,
            Screen.currentResolution.height,
            Screen.dpi,
            Screen.fullScreen,
            SystemInfo.graphicsDeviceName,
            SystemInfo.graphicsDeviceVendor,
            SystemInfo.graphicsMemorySize,
            SystemInfo.graphicsPixelFillrate,
            SystemInfo.maxTextureSize,

            Application.loadedLevelName,
            Application.unityVersion,
            GameSettings.AdsDisabled
        );

        //if (Main.Player != null) {
        //    errorMessage.Append("\n\n ***PLAYER*** \n");
        //    errorMessage.Append(XamlServices.Save(Main.Player));
        //}

        try {
            using (var client = new WebClient()) {
                var arguments = new NameValueCollection();
                //if (loginResult != null)
                //    arguments.Add("SessionId", loginResult.SessionId.ToString());
                arguments.Add("report", errorMessage.ToString());
                var result = Encoding.ASCII.GetString(client.UploadValues(serviceAddress + "/ReportCrash", arguments));
                //Debug.Log(result);
            }
        } catch (WebException e) {
            Debug.Log("Report Crash: " + e.ToString());
        }
    }
Run Code Online (Sandbox Code Playgroud)


Biz*_*han 6

将此脚本附加到场景中的空游戏对象:

using UnityEngine;
public class ExceptionManager : MonoBehaviour
{
    void Awake()
    {
        Application.logMessageReceived += HandleException;
        DontDestroyOnLoad(gameObject);
    }

    void HandleException(string logString, string stackTrace, LogType type)
    {
        if (type == LogType.Exception)
        {
             //handle here
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

确保有一个实例.


Dee*_*orn 5

Unity开发人员只是不向我们提供类似的工具。它们在框架内部和外部捕获异常,并将它们记录为字符串,从而为我们提供Application.logMessageReceived [Threaded]。因此,如果您需要发生异常或以自己的处理方式(而不是统一的方式)记录日志,我可以想到:

  1. 不要使用框架机制,而是使用自己的机制,因此框架不会捕获异常

  2. 使自己的类实现UnityEngine.ILogHandler

    public interface ILogHandler
    {
        void LogFormat(LogType logType, Object context, string format, params object[] args);
        void LogException(Exception exception, Object context);
    }
    
    Run Code Online (Sandbox Code Playgroud)

并按照官方文档中的说明使用它来记录您的异常。但是这样一来,您就不会收到未处理的异常和从插件记录的异常(是的,有人在框架中记录了异常而不是抛出异常)

  1. 或者,您可以提出建议/要求团结Debug.unityLoggerDebug.logger在Unity 2017中已弃用)具有setter或其他机制,以便我们可以通过自己的机制。

  2. 只需使用反射进行设置即可。但这是暂时的破解,当统一更改代码时将无法使用。

    var field = typeof(UnityEngine.Debug)
        .GetField("s_Logger", BindingFlags.Static | BindingFlags.NonPublic);
    field.SetValue(null, your_debug_logger);
    
    Run Code Online (Sandbox Code Playgroud)

注意:要获得正确的堆栈跟踪,您需要在编辑器设置/代码中将StackTraceLogType设置为ScriptOnly(大多数情况下,我写了一篇有关其工作原理的文章),并且在为iOS进行构建时,据说必须进行脚本调用优化设置为缓慢而安全

如果有兴趣,您可以阅读流行的崩溃分析工具的工作原理。如果您研究crashlytics(适用于android / ios的崩溃报告工具),那么您会发现它在内部使用Application.logMessageReceivedAppDomain.CurrentDomain.UnhandledException事件来记录托管C#异常。

如果对有关统一框架捕获异常的示例感兴趣,则可以查看ExecuteEvents.Update。另外,我在此处提供了另一篇有关在按钮单击侦听器中测试其捕获异常的文章。

有关记录未处理异常的官方方法的一些摘要:

I.当主线程发生异常时,将触发Application.logMessageReceived。有几种方法可以实现:

  1. C#代码中捕获异常并通过登录 Debug.LogException
  2. 本机代码(使用il2cpp时可能是c ++代码)捕获了异常。在这种情况下,本机代码调用Application.CallLogCallback会导致触发Application.logMessageReceived

注意:当原始异常具有内部异常时,StackTrace字符串将包含“重新抛出”

二。Application.logMessageReceivedThreaded在任何线程(包括main)上发生异常时会触发(注意docs中提到)注意:它必须是线程安全的

三,AppDomain.CurrentDomain.UnhandledException例如在以下情况下被解雇:

您在编辑器中调用以下代码:

 new Thread(() =>
    {
        Thread.Sleep(10000);
        object o = null;
        o.ToString();
    }).Start();
Run Code Online (Sandbox Code Playgroud)

但是使用Unity 5.5.1f1时,它会导致android 4.4.2发行版崩溃

注意:在记录异常和断言时,我重现了一些错误,这些错误导致统一缺少堆栈框架。我submited 一个人。


Jer*_*dak 2

您提到Application.RegisterLogCallback,您尝试过实现它吗?因为日志记录回调会传回堆栈跟踪、错误和错误类型(警告、错误等)。

您上面概述的策略很难实施,因为 MonoBehaviours 不只有一个入口点。您必须处理 OnTriggerEvent、OnCollisionEvent、OnGUI 等。每个都将其逻辑包装在异常处理程序中。

恕我直言,异常处理在这里是一个坏主意。如果您不立即重新抛出异常,您最终将以奇怪的方式传播这些错误。也许 Foo 依赖于 Bar,而 Bar 又依赖于 Baz。假设 Baz 抛出一个被捕获并记录的异常。然后 Bar 会抛出异常,因为它需要从 Baz 获取的值不正确。最后 Foo 抛出异常,因为它从 Bar 获取的值无效。