30

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

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

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

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捕获日志消息(而不是异常)的建议 ,但这让我感到不舒服,因为我需要解析调试日志字符串而不是访问实际的异常和堆栈跟踪。

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

4

5 回答 5

19

在场景中创建一个空的 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),通过防止它在场景变化的情况下被破坏,使这个 GameObject 持久化。

于 2017-01-10T08:10:58.697 回答
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);
        }
    }

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

        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());
        }
    }
于 2014-06-03T16:37:54.857 回答
6

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

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

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

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

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

注意:要获得正确的堆栈跟踪,您需要将编辑器设置/代码中的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 字符串将包含“rethrow”

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

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

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

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

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

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

于 2017-05-06T14:42:48.053 回答
2

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

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

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

于 2013-02-25T15:13:23.643 回答
0

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

于 2016-01-08T10:54:59.123 回答