C# Unity3D 的一个好的全局异常处理策略是什么?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/15061050/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-10 13:54:53  来源:igfitidea点击:

What's a good global exception handling strategy for Unity3D?

c#unity3d

提问by theodox

I'm looking into doing some Unity3D scripting stuff, and I'd like to set up global exception handling system. This is not for running in the release version of the game, the intention is to catch exceptions in user scripts and also in editor scripts and make sure they are forwarded to a database for analysis (and also to send email to relevant devs so they can fix their shizzle).

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

In a vanilla C# app I'd have a try-catch around the Main method. In WPF I'd hook one or more of the unhandled exception events. In Unity...?

在普通的 C# 应用程序中,我会对 Main 方法进行尝试。在 WPF 中,我会挂钩一个或多个未处理的异常事件。在统一...?

So far the best I've been able to come up with is something like this:

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

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

}

In other scripts, I derive BehaviourBase instead of MonoBehavior and implement performUpdate() instead of Update(). I haven't implemented a parallel version for Editor clases but I assume I'd have to do the same thing there.

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

I don't like this strategy, however, because I'll have to backport it to any scripts we grab from the community (and I'll have to enforce it on the team). The Editor scripts don't have a single point of entry comparable to MonoBehavior either, so I assume I'd have to implement exception safe versions of wizards, editors and so on.

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

I've seen suggestions about catching log messages (as opposed to exceptions) using Application.RegisterLogCallback, but this makes me uncomfortable because I'd need to parse the debug log string rather than having access to the actual exceptions and stacktraces.

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

So... what's the right thing to do?

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

回答by Jerdak

You mentioned Application.RegisterLogCallback, have you tried implementing it? Because the logging callbackpasses back a stack trace, an error, and an error type (warning, error, etc).

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

The strategy you outline above would be tough to implement because MonoBehaviours don't just have a single entry point. You'd have to handle OnTriggerEvent, OnCollisionEvent, OnGUI, and so on. Each one wrapping its logic in an exception handler.

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

IMHO, exception handling is a bad idea here. If you don't immediately re-throw the exception, you'll end up propagating those errors in weird ways. Maybe Foo relies on Bar, and Bar on Baz. Say Baz throws an exception that is caught and logged. Then Bar throws an exception because the value it needs from Baz is incorrect. Finally Foo throws an exception because the value it was getting from Bar is invalid.

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

回答by Bryan Legend

There is a working implementation of RegisterLogCallback that I found here: http://answers.unity3d.com/questions/47659/callback-for-unhandled-exceptions.html

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

In my own implementation I use it to call my own MessageBox.Show instead of writing to a log file. I just call SetupExceptionHandling from each of my scenes.

在我自己的实现中,我使用它来调用我自己的 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);
        }
    }

I also now have the error handler email me via this routine, so I always know when my app crashes and get as much detail as possible.

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

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

回答by yusuf baklac?

You can use a plugin called Reporterto receive an email of Debug Logs, Stack trace and screen capture on the moment of unhandled Error. Screen capture and stack trace are usually enough to figure out the reason of the Error. For stubborn sneaky Errors you should log more of suspicious data, build and wait again for the error.I Hope this helps.

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

回答by Bizhan

Create an empty GameObject in your scene and attach this script to it:

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

make sure there is one instance.

确保有一个实例

The rest is up to you. You can also store the logs in file system, web serveror cloud storage.

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



Note that DontDestroyOnLoad(gameObject)makes this GameObject persistent, by preventing it from being destroyed in case of scene change.

请注意DontDestroyOnLoad(gameObject),通过防止它在场景更改时被破坏,可以使此 GameObject 持久化。

回答by Deepscorn

Unity devs just do not provide us with tools like that. They catch exceptions internally in framework here and there and log them as strings, giving us Application.logMessageReceived[Threaded]. So, if you need exceptions to happen or be logged with your own processing (not unity's) I can think of:

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

  1. do not use framework mechanics, but use your own so exception is not caught by framework

  2. make your own class implementing UnityEngine.ILogHandler:

    public interface ILogHandler
    {
        void LogFormat(LogType logType, Object context, string format, params object[] args);
        void LogException(Exception exception, Object context);
    }
    
  1. 不要使用框架机制,而是使用自己的机制,这样框架就不会捕获异常

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

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

And use it as said in official docsto log your exceptions. But that way you do not receive unhandled exceptions and exceptions logged from plugins (yes, someone do log exceptions in frameworks instead of throwing them)

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

  1. Or you can make a suggestion/request to unity to make Debug.unityLogger(Debug.loggeris deprecated in Unity 2017) have setter or other mechanism so we can pass our own.

  2. Just set it with reflection. But it's temporary hack and will not work when unity change code.

    var field = typeof(UnityEngine.Debug)
        .GetField("s_Logger", BindingFlags.Static | BindingFlags.NonPublic);
    field.SetValue(null, your_debug_logger);
    
  1. 或者您可以向 unity 提出建议/请求Debug.unityLoggerDebug.logger在 Unity 2017 中已弃用)具有 setter 或其他机制,以便我们可以通过我们自己的。

  2. 只需用反射设置它。但这是暂时的 hack,当 unity 更改代码时将不起作用。

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

Note: To get correct stacktraces you need to set StackTraceLogTypein editor settings/code to ScriptOnly (most times it's what you need, I wrote an articleon how it work) And, when building for iOS, it is said that Script call optimization must be set to slow and safe

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

If interested, you can read how popular crash analytics tool works. If you look into crashlytics (crash report tool for android/ios), than you'll find out that it internally uses Application.logMessageReceivedand AppDomain.CurrentDomain.UnhandledExceptionevents to log managed C# exceptions.

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

If interested in examples on unity framework catching exceptions, you may look at ExecuteEvents.UpdateAnd another article from me with testing it catching exception in button click listener can be found here.

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

Some summary on official ways to log unhandled exception:

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

I. Application.logMessageReceived is fired when exception happens on main thread. There are ways for it to happen:

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

  1. exception caught in c# code and logged through Debug.LogException
  2. exception caught in native code (probably c++ code when using il2cpp). In that case native code calls Application.CallLogCallbackwhich results in firing Application.logMessageReceived
  1. 在 C# 代码中捕获并记录的异常 Debug.LogException
  2. 本机代码中捕获的异常(使用 il2cpp 时可能是 C++ 代码)。在这种情况下,本机代码调用Application.CallLogCallback会导致触发Application.logMessageReceived

Note: StackTrace string will contain "rethrow" when original exception have inner exceptions

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

II. Application.logMessageReceivedThreadedis fired when exception happens on any thread, including main (it's said in docs) Note: it must be thread-safe

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

III. AppDomain.CurrentDomain.UnhandledExceptionfor example is fired when:

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

You call the following code in editor:

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

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

But it causes crash on android 4.4.2 release build when using Unity 5.5.1f1

但是当使用 Unity 5.5.1f1 时,它会导致在 android 4.4.2 release build 上崩溃

Note: I reproduced some bugs with unity missing stackframes when logging exceptions and assertions. I submited oneof them.

注意:我在记录异常和断言时重现了一些带有统一丢失堆栈帧的错误。我submited一个人。