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

时间:2013-02-25 06:06:08

标签: c# unity3d

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

在一个vanilla C#应用程序中,我有一个围绕Main方法的try-catch。在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()。我没有为Editor clases实现并行版本,但我认为我必须在那里做同样的事情。

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

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

那么......做什么是正确的?

5 个答案:

答案 0 :(得分:8)

我在这里找到了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());
        }
    }

答案 1 :(得分:5)

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

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

    void HandleException(string condition, string stackTrace, LogType type)
    {
        if (type == LogType.Exception)
        {
             //handle here
        }
    }
}

确保有一个实例。

答案 2 :(得分:3)

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

  1. 不使用框架机制,但是使用自己的框架机制,因此框架不会捕获

  2. 让你自己的类实现UnityEngine.ILogHandler:

    公共接口ILogHandler   {     void LogFormat(LogType logType,Object context,string format,params object [] args);     void LogException(异常异常,Object context);   }

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

    1. 或者您可以向Unity提出建议/请求以使Debug.unityLogger(在Unity 2017中不推荐使用Debug.logger)具有setter或其他机制,以便我们可以通过我们自己的。

    2. 只需用反射设置即可。但它是暂时的黑客攻击,并且在统一改变代码时不会起作用。

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

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

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

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

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

      予。在主线程上发生异常时会触发Application.logMessageReceived。有很多方法可以实现:

      1. 在c#代码中捕获并通过Debug.LogException
      2. 记录的异常
      3. 在本机代码中捕获的异常(使用il2cpp时可能是c ++代码)。在这种情况下,本机代码调用Application.CallLogCallback,这会导致触发Application.logMessageReceived
      4. 注意:stacktrace字符串将包含" rethrow"当原始异常有内部异常时

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

        III。例如,在以下情况下触发AppDomain.CurrentDomain.UnhandledException:

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

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

        但是当使用Unity 5.5.1f1

        时,它会导致android 4.4.2发布版本崩溃

        注意:在记录异常和断言时,我复制了一些缺少stackframes的错误。我提交了one他们。

答案 3 :(得分:2)

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

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

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

答案 4 :(得分:0)

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