什么';对于Unity3D来说,这是一个很好的全局异常处理策略

本文关键字:一个 很好 策略 异常处理 全局 对于 Unity3D 来说 什么 | 更新日期: 2023-09-27 18:01:00

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

在一个普通的C#应用程序中,我会尝试绕过Main方法。在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();
}

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

然而,我不喜欢这种策略,因为我必须将其移植到我们从社区获取的任何脚本中(我必须在团队中强制执行)。编辑器脚本也没有一个可与MonoBehavior相媲美的入口点,所以我认为我必须实现向导、编辑器等的异常安全版本

我看到了关于使用应用程序捕获日志消息(而不是异常)的建议。RegisterLogCallback,但这让我感到不舒服,因为我需要解析调试日志字符串,而不是访问实际的异常和堆栈争用。

所以。。。正确的做法是什么?

什么';对于Unity3D来说,这是一个很好的全局异常处理策略

在场景中创建一个空的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
        }
    }
}

请确保存在一个实例

剩下的由你决定。您还可以将日志存储在文件系统web服务器云存储中。


请注意,DontDestroyOnLoad(gameObject)通过防止该游戏对象在场景更改时被破坏,使其持久化。

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

在我自己的实现中,我使用它来调用我自己的MessageBox。显示而不是写入日志文件。我只是在每个场景中调用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());
        }
    }

Unity开发人员没有为我们提供这样的工具。他们在框架内部到处捕捉异常,并将其记录为字符串,从而为我们提供Application.logMessageReceived[Threaded]。因此,如果您需要发生异常或使用自己的处理(而不是unity)记录异常,我可以想到:

  1. 不要使用框架机制,而是使用您自己的机制,这样异常就不会被框架捕获

  2. 制作自己的类来实现UnityEngine.ILogHandler:

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

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

  1. 或者你可以向unity提出建议/请求,让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);
    

注意:为了获得正确的堆栈跟踪,你需要在编辑器设置/代码中将StackTraceLogType设置为ScriptOnly(大多数时候这就是你需要的,我写了一篇关于它如何工作的文章)。而且,在为iOS构建时,据说脚本调用优化必须设置为慢速和安全的

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

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

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

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

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

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

II。Application.logMessageReceivedThreaded在任何线程上发生异常时都会被激发,包括main(在文档中有这样的说法)注意:它必须是线程安全的

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

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

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

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

注意:在记录异常和断言时,我复制了一些缺少统一堆栈框架的错误。我提交了其中一份。

您提到了应用程序。RegisterLogCallback,你试过实现它吗?因为日志回调传递回堆栈跟踪、错误和错误类型(警告、错误等)。

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

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

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