【问题标题】:What's a good global exception handling strategy for Unity3D?Unity3D 有什么好的全局异常处理策略?
【发布时间】:2013-02-25 06:06:08
【问题描述】:

我正在考虑做一些 Unity3D 脚本编写工作,并且我想设置全局异常处理系统。这不是为了在游戏的发布版本中运行,其目的是在用户脚本和编辑器脚本中捕获异常,并确保将它们转发到数据库进行分析(以及向相关开发人员发送电子邮件,以便他们可以解决他们的问题)。

在一个普通的 C# 应用程序中,我会尝试使用 Main 方法。在 WPF 中,我会挂钩一个或多个 unhandled exception events。在 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();

}

在其他脚本中,我派生 BehaviourBase 而不是 MonoBehavior,并实现 performUpdate() 而不是 Update()。我还没有为编辑器类实现并行版本,但我认为我必须在那里做同样的事情。

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

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

那么……正确的做法是什么?

【问题讨论】:

    标签: c# unity3d


    【解决方案1】:

    在您的场景中创建一个空的 GameObject 并将此脚本附加到它:

    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
            }
        }
    }
    

    确保有一个实例

    剩下的就看你自己了。您还可以将日志存储在文件系统网络服务器云存储中。


    注意DontDestroyOnLoad(gameObject) 使这个游戏对象持久化,防止它在场景改变的情况下被破坏。

    【讨论】:

    • 到目前为止这对我来说效果很好,在运行时有效
    【解决方案2】:

    我在这里找到了一个有效的 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);
            }
        }
    

    我现在也有错误处理程序通过此例程向我发送电子邮件,因此我始终知道我的应用程序何时崩溃并获得尽可能多的详细信息。

            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());
            }
        }
    

    【讨论】:

    • 你能告诉我你如何调用reportcrash,你会解析一个特定的字符串吗?
    【解决方案3】:

    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);
      }
      

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

    1. 或者您可以向 unity 提出建议/请求,以使 Debug.unityLoggerDebug.logger 在 Unity 2017 中已弃用)具有设置器或其他机制,以便我们可以通过自己的机制。

    2. 只需用反射设置它。但它是临时 hack,当统一更改代码时将不起作用。

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

    注意:要获得正确的堆栈跟踪,您需要在编辑器设置/代码中将 StackTraceLogType 设置为 ScriptOnly(大多数情况下这是您需要的,我写了一个 article 来说明它是如何工作的)而且,在为 iOS 构建时,它据说Script call optimization must be set to slow and safe

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

    如果对统一框架捕获异常的示例感兴趣,您可以查看ExecuteEvents.Update 我的另一篇测试它在按钮单击侦听器中捕获异常的文章可以找到here

    关于记录未处理异常的官方方法的一些总结:

    我。 Application.logMessageReceived 在主线程发生异常时被触发。有几种方法可以实现:

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

    注意:当原始异常有内部异常时,StackTrace 字符串将包含“rethrow”

    二。 Application.logMessageReceivedThreaded 在任何线程(包括主线程)发生异常时被触发(在docs 中说过)注意:它必须是线程安全的

    三。 AppDomain.CurrentDomain.UnhandledException 例如在以下情况下被触发:

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

     new Thread(() =>
        {
            Thread.Sleep(10000);
            object o = null;
            o.ToString();
        }).Start();
    

    但在使用 Unity 5.5.1f1 时会导致 android 4.4.2 版本崩溃

    注意:我在记录异常和断言时重现了一些缺少统一堆栈帧的错误。我提交了其中的one

    【讨论】:

    • "Application.logMessageReceivedThreaded 在任何线程发生异常时被触发"。这是否意味着 Application.logMessageReceivedThreaded 也可以捕获 AppDomain.CurrentDomain.UnhandledException 捕获的内容?
    【解决方案4】:

    您提到了 Application.RegisterLogCallback,您是否尝试过实现它?因为logging callback 传回了堆栈跟踪、错误和错误类型(警告、错误等)。

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

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

    【讨论】:

    • 我明白你的意思,但这并不适用于运行时——它旨在确保我们团队的工具包得到彻底调试——这意味着我想捕捉每一个异常并系统地记录它;我可能还想借此机会记录不在堆栈跟踪中的上下文信息。我对捕获不感兴趣只是为了继续前进 - 我对捕获感兴趣是为了对来自许多用户(通常不在同一建筑物中)的错误进行远程取证。
    • @theodox 不,我明白你的意思,我不是专门指发布版本。但是,如果您添加异常处理,一种或另一种方式,它就在那里。因此,除非您要维护 2 个独立的代码库或非常糟糕的 BehaviourBase,否则我怀疑是否会有一个快速简便的解决方案。
    【解决方案5】:

    您可以使用名为Reporter 的插件在出现未处理错误时接收有关调试日志、堆栈跟踪和屏幕截图的电子邮件。屏幕截图和堆栈跟踪通常足以找出错误的原因。对于顽固的偷偷摸摸的错误,您应该记录更多可疑数据,构建并再次等待错误。我希望这会有所帮助。

    【讨论】:

      猜你喜欢
      • 2010-10-09
      • 2011-01-23
      • 2016-02-14
      • 2016-11-03
      • 1970-01-01
      • 1970-01-01
      • 2012-04-23
      • 2021-11-12
      • 2014-07-03
      相关资源
      最近更新 更多